16511 lines
1.1 MiB
16511 lines
1.1 MiB
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": []
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Extracting Text from EPUB Files"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Requirement already satisfied: ebooklib in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (0.18)\n",
|
||
"Requirement already satisfied: beautifulsoup4 in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (4.12.3)\n",
|
||
"Requirement already satisfied: lxml in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (5.3.0)\n",
|
||
"Requirement already satisfied: pyperclip in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (1.9.0)\n",
|
||
"Requirement already satisfied: PyMuPDF in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (1.24.11)\n",
|
||
"Requirement already satisfied: nltk in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (3.9.1)\n",
|
||
"Requirement already satisfied: six in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (from ebooklib) (1.16.0)\n",
|
||
"Requirement already satisfied: soupsieve>1.2 in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (from beautifulsoup4) (2.5)\n",
|
||
"Requirement already satisfied: click in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (from nltk) (8.1.7)\n",
|
||
"Requirement already satisfied: joblib in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (from nltk) (1.4.2)\n",
|
||
"Requirement already satisfied: regex>=2021.8.3 in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (from nltk) (2024.9.11)\n",
|
||
"Requirement already satisfied: tqdm in /home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages (from nltk) (4.66.4)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"!python -m pip install ebooklib beautifulsoup4 lxml pyperclip PyMuPDF nltk"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"/home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages/ebooklib/epub.py:1395: UserWarning: In the future version we will turn default option ignore_ncx to True.\n",
|
||
" warnings.warn('In the future version we will turn default option ignore_ncx to True.')\n",
|
||
"/home/ys/Data/projects/code/Python/Envs/General/lib/python3.11/site-packages/ebooklib/epub.py:1423: FutureWarning: This search incorrectly ignores the root element, and will be fixed in a future version. If you rely on the current behaviour, change it to './/xmlns:rootfile[@media-type]'\n",
|
||
" for root_file in tree.findall('//xmlns:rootfile[@media-type]', namespaces={'xmlns': NAMESPACES['CONTAINERNS']}):\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
" ForewordBY JAMES K. GALBRAITHTOWARD THE END of this account, my father\n",
|
||
"brought himself, with a show of reluctance that he knew would not be taken too\n",
|
||
"seriously, to comment on whether another crash would come. There were many\n",
|
||
"reasons, including memory and new regulation, why not. But then: No one can\n",
|
||
"doubt that the American people remain susceptible to the speculative mood—to the\n",
|
||
"conviction that enterprise can be attended by unlimited rewards in which they,\n",
|
||
"individually, were meant to share. A rising market can still bring the reality\n",
|
||
"of riches. This, in turn, can draw more and more people to participate. The\n",
|
||
"government preventatives and controls are ready. In the hands of a determined\n",
|
||
"government their efficacy cannot be doubted. There are, however, a hundred\n",
|
||
"reasons why a government will determine not to use them. The main relevance of\n",
|
||
"The Great Crash, 1929 to the Great Crisis of 2008 is surely here. In both cases,\n",
|
||
"the government knew what it should do. Both times, it declined to do it. In the\n",
|
||
"summer of 1929 a few stern words from on high, a rise in the discount rate, a\n",
|
||
"tough investigation into the pyramid schemes of the day, and the house of cards\n",
|
||
"on Wall Street would have tumbled before its fall destroyed the whole economy.\n",
|
||
"In 2004 the FBI warned publicly of \"an epidemic of mortgage fraud.\" But the\n",
|
||
"government did nothing, and less than nothing, delivering instead low interest\n",
|
||
"rates, deregulation, and clear signals that laws would not he enforced. This was\n",
|
||
"fuel for fires. The Greenspan Doctrine held that bubbles cannot be prevented,\n",
|
||
"that the governments task is merely to clean up afterward. The Greenspan\n",
|
||
"practice was to create one bubble after another, until finally one came along so\n",
|
||
"vast that it destroyed the system on the way by. At its source, our crisis is a\n",
|
||
"housing crisis, and not one of too little housing but of too much. It springs\n",
|
||
"from a predatory attack on the safeguards that had for decades kept housing\n",
|
||
"finance safe and stable. In the early 2000s the Bush administration sent clear\n",
|
||
"signals that regulations on mortgages would not be enforced. The signals were\n",
|
||
"not subtle: on one occasion the director of the Office of Thrift Supervision\n",
|
||
"came to a press conference with copies of the Federal Register and a chain saw.\n",
|
||
"There followed every manner of scheme to fleece the unsuspecting—\"liars' loans,\"\n",
|
||
"\"no-doc loans,\" and \"neutron loans\" were terms of art in the business—bundled\n",
|
||
"together, rated and securitized, then spread through the world and left to\n",
|
||
"fester until rising interest rates and crashing prices wrecked the system.\n",
|
||
"Richard Cohen of the Washington Post penned a memorable account of one case, a\n",
|
||
"Marvene Halterman of Avondale, Arizona: At age 61, after 13 years of\n",
|
||
"uninterrupted unemployment and at least as many years of living on welfare, she\n",
|
||
"got a mortgage ... She got it even though at one time she had 23 people living\n",
|
||
"in the house (576 square feet, one bath) and some ramshackle outbuildings. She\n",
|
||
"got it for $103,000, an amount that far exceeded the value of the house. The\n",
|
||
"place has since been condemned ... Halterman's house was never exactly a\n",
|
||
"showcase—the city had since cited her for all the junk (clothes, tires, etc.) on\n",
|
||
"her lawn. Nonetheless, a local financial institution with the cover-your-wallet\n",
|
||
"name of Integrity Funding LLC gave her a mortgage, valuing the house at about\n",
|
||
"twice what a nearby and comparable property sold for ... Integrity Funding then\n",
|
||
"sold the loan to Wells Fargo & Co., which sold it to HSBC Holdings PLC, which\n",
|
||
"then packaged it with thousands of other risky mortgages and offered the\n",
|
||
"indigestible porridge to investors. Standard & Poor's and Moody's Investors\n",
|
||
"Service took a look at it all, as they are supposed to do, and pronounced it\n",
|
||
"\"triple-A.\" This was fraud, perpetrated in the first instance by the government\n",
|
||
"on the population, and by the rich on the poor. The borrowers were, of course,\n",
|
||
"complicit in some cases, taking out mortgages they never had a hope of paying\n",
|
||
"down. In many more, they were simply naive, gullible, open to pressure,\n",
|
||
"credulous, and hopeful—something might turn up. They took the assurances of\n",
|
||
"lenders that housing prices always rise, that bad loans can always be\n",
|
||
"refinanced. They were attractive to lenders for all of these reasons, and\n",
|
||
"because they had nothing to lose. A borrower with nothing to lose will sign\n",
|
||
"papers that another will not. A government that permits this to happen is\n",
|
||
"complicit in a vast crime.As in 1929, the architects of disaster will form a\n",
|
||
"rich rogues gallery to go shooting in. The Old Objectivist, Alan Greenspan, was\n",
|
||
"intermittently aware of impending disaster and resolutely unwilling to stop it.\n",
|
||
"The Liberal's Banker, Robert Rubin, had a reputation for fiscal probity eclipsed\n",
|
||
"by catastrophic complacency at Citigroup, where he was paid SI 15 million and\n",
|
||
"maintained silence, so far as we know. There will be Phil Gramm, of whom in\n",
|
||
"April 2008 the Washington Post wrote that he was \"the sorcerers apprentice of\n",
|
||
"financial instability and disaster.\" (The Post was quoting me. Reached on the\n",
|
||
"phone, Gramm denied it.) There will be Lawrence Summers, impassioned advocate of\n",
|
||
"the repeal of the Glass-Steagall Act in 1999, bounced from Harvard's presidency\n",
|
||
"to Obama's White House—a man whose reputation remains to be rebuilt or buried by\n",
|
||
"events. And Bernard Madoff. The wreckage of bloated reputations is part of the\n",
|
||
"fun of a crash, at least in deep retrospect. And in the renewed prosperity of\n",
|
||
"the 1950s and later years, my father and history were generally content to leave\n",
|
||
"matters there. Of the figures here told of, Charles Mitchell and Samuel Insull\n",
|
||
"were acquitted, Ivar Kreuger committed suicide, and only Richard Whitney entered\n",
|
||
"Sing-Sing. Juries in our time, if they get the chance, will be less forgiving.\n",
|
||
"Whether they will get the chance is another matter. If they don't, there is not\n",
|
||
"much hope to clean up after the colossal crimes of the past decade. And so far,\n",
|
||
"if the Obama presidency harbors the Roosevelt who gave us the Emergency Banking\n",
|
||
"Act and the SEC, he has not yet come into the open.But there is time, and we\n",
|
||
"shall see.Largely missing from a book on the Great Crisis, 2008, will be the\n",
|
||
"elements of hope, credulity, and carefree optimism that were redeeming features\n",
|
||
"of the 1920s boom. It made people very happy at the time. We read in these pages\n",
|
||
"Frederick Lewis Allen's account of the rich man's chauffeur, his \"ears laid\n",
|
||
"back\" for news of a movement in Bethlehem Steel, and the Wyoming cattleman who\n",
|
||
"traded a thousand shares a day. In 1929, millions thought they could easily\n",
|
||
"become rich, and some did. The parallel moment in modern history came in the\n",
|
||
"late 1990s, in the information-technology boom under President Clinton, which\n",
|
||
"dissolved at the turn of the decade. The years after Bush v. Gore —the years of\n",
|
||
"9/11, the war on terror, and the invasion of Iraq—had no element of this. The\n",
|
||
"masseuse who put her life savings into Madoff's hands wasn't trying to get rich;\n",
|
||
"she was hoping only for a safe and steady return. Millions of others, who took\n",
|
||
"out mortgages, were asking for even less. They were not, in the main,\n",
|
||
"speculators. What they wanted, most of all, was just what everyone else already\n",
|
||
"had: a home of their own. The trouble for them was, there was no way to get one\n",
|
||
"without making a speculative bet. Now they are losing their homes by the\n",
|
||
"millions—an American tragedy, as families double up, move to rentals, crowd into\n",
|
||
"motel rooms or their cars, or spill out into city parks. The victims of the bust\n",
|
||
"extend across the American middle class, to millions of prime credits with half-\n",
|
||
"paid loans and 401(k) plans and a little cash, whose home equity, stock\n",
|
||
"holdings, and interest earnings all collapsed. What in 1930 took the dramatic\n",
|
||
"form of runs on the bank—the wiping out of middle-class savings and wealth—in\n",
|
||
"2008 and 2009 took the form of a simultaneous, month-over-month collapse in\n",
|
||
"asset values, followed by the bankruptcy, downsizing, and liquidation of\n",
|
||
"business firms. In this there was often less immediate hardship—the country is\n",
|
||
"not short of food—than the drying up of possibilities—for college, work, and\n",
|
||
"retirement. This too casts a pall over our time. Just as the years before the\n",
|
||
"recent crisis were not joyful, the world after it risks being less acutely\n",
|
||
"desperate, and more merely dreary, than the age of Roosevelt and the New\n",
|
||
"Deal.Whether the events following the crisis will track the disasters of 1930\n",
|
||
"and later years is also doubtful. In our day of big government—with what\n",
|
||
"economists call automatic stabilizers and immediate fiscal stimulus—slumps are\n",
|
||
"milder and recoveries come sooner than was the case almost a century back. It\n",
|
||
"took four years for the United States to turn to Roosevelt, while by an accident\n",
|
||
"of political timing it was able to elect and install Barack Obama within just a\n",
|
||
"few months. Correspondingly, the situation facing the country today is less\n",
|
||
"dire, the consensus behind radical action is weaker, and the instincts of the\n",
|
||
"administration are less heroic.As this is written, the broad economy seems to\n",
|
||
"have stabilized, and experts are debating whether the stock revival of early\n",
|
||
"2009 is the start of recovery or a \"suckers rally.\" Production may resume, but\n",
|
||
"with an ever larger share coming from imports. Yet things could still go very\n",
|
||
"wrong. The administration seems determined to keep every toxic bank and\n",
|
||
"insurance company alive and intact. No one expects an early revival of\n",
|
||
"employment, and no plans are afoot that would quickly reemploy the millions who\n",
|
||
"are losing their jobs. The Great Crisis has so far not produced its Harry\n",
|
||
"Hopkins, Harold Ickes, Frances Perkins, Henry Wallace, and the others whose\n",
|
||
"eventual emergence is implicit here. For all these reasons, it seems altogether\n",
|
||
"unlikely that the books to come on the Great Crisis of 2008 will be as much fun\n",
|
||
"as this one. A final word. Readers of The Great Crash, 1929 may have wondered\n",
|
||
"about the investing habits of its author, and in particular whether (like\n",
|
||
"Keynes) he was ever tempted by the sirens of financial speculation. I can report\n",
|
||
"that so far as I know he was not. His portfolio was that of a value investor, a\n",
|
||
"buy-and-holder for a retirement he never actually took, as he wrote on, happily\n",
|
||
"and profitably, practically until his death, at ninety-seven, in April 2006.He\n",
|
||
"was also not averse, on occasion, to even stricter precaution. I remember\n",
|
||
"calling him on the night of the market break in October 1987, an event that\n",
|
||
"brought this book back from a brief disappearance. It was, due to the attentions\n",
|
||
"of the national press, somewhat difficult to get through on the phone. But when\n",
|
||
"I did, I heard the reassuring paternal voice:\"James? Is that you?\" Pause. \"Not\n",
|
||
"to worry: I've been in cash for three weeks.\"But then there was another pause,\n",
|
||
"and the tone changed.\"But I'm sorry to say, the same cannot be said of your\n",
|
||
"mother. She finds it very difficult to sell the General Electric that her family\n",
|
||
"bought from Edison for a dollar.\"AUSTIN, TEXAS MAY 18, 2009\n",
|
||
"IntroductionThe View from the NinetiesThe Great Crash, 1929 was first published\n",
|
||
"in 1955 and has been continuously in print ever since, a matter now of forty\n",
|
||
"years and more. Authors (and publishers) being as they are, the tendency is to\n",
|
||
"attribute this endurance to the excellence of the work. Evidently this book has\n",
|
||
"some merit, but, for worse or perhaps better, there is another reason for its\n",
|
||
"durability. Each time it has been about to pass from print and the bookstores,\n",
|
||
"another speculative episode—another bubble or the ensuing misfortune—has stirred\n",
|
||
"interest in the history of this, the great modern case of boom and collapse,\n",
|
||
"which led on to an unforgiving depression.One of the subsequent episodes\n",
|
||
"occurred, in fact, as the book was coming from the printer. There was a small\n",
|
||
"stock market boom in the spring of 1955; I was called to Washington to testily\n",
|
||
"at a Senate hearing on the past experience. During my testimony that morning the\n",
|
||
"stock market went suddenly south. I was blamed for the collapse, especially by\n",
|
||
"those who were long in the market. A fair number of the latter wrote to threaten\n",
|
||
"me with physical injury; a more devout citizenry told me they were praying for\n",
|
||
"my ill health or early demise. A few days after my testimony I broke my leg\n",
|
||
"while skiing in Vermont. The papers carried mention. Letters came in telling me\n",
|
||
"of prayers that had been answered. I had at least done something for religion.\n",
|
||
"In the mood of the times a senator from Indiana, Homer E. Capehart, said it was\n",
|
||
"the work of a crypto-Communist.That was only the beginning. The offshore-funds\n",
|
||
"insanity of the seventies, the big bust of 1987, less dramatic episodes or\n",
|
||
"fears, all brought attention back to 1929 and kept the book in print. And so\n",
|
||
"again now in 1997. That we are having a major speculative splurge as this is\n",
|
||
"written is obvious to anyone not captured by vacuous optimism. There is now far\n",
|
||
"more money flowing into the stock markets than there is intelligence to guide\n",
|
||
"it. There are many more mutual funds than there are financially acute,\n",
|
||
"historically aware men and women to manage them. I am not given to prediction;\n",
|
||
"one's foresight is forgotten, only one's errors are well remembered. But there\n",
|
||
"is here a basic and recurrent process. It comes with rising prices, whether of\n",
|
||
"stocks, real estate, works of art or anything else. This increase attracts\n",
|
||
"attention and buyers, which produces the further effect of even higher prices.\n",
|
||
"Expectations are thus justified by the very action that sends prices up. The\n",
|
||
"process continues; optimism with its market effect is the order of the day.\n",
|
||
"Prices go up even more. Then, for reasons that will endlessly be debated, comes\n",
|
||
"the end. The descent is always more sudden than the increase; a balloon that has\n",
|
||
"been punctured does not deflate in an orderly way.To repeat, I make no\n",
|
||
"prediction; I only observe that this phenomenon has manifested itself many times\n",
|
||
"since 1637, when Dutch speculators saw tulip bulbs as their magic road to\n",
|
||
"wealth, and 1720, when John Law brought presumptive wealth and then sudden\n",
|
||
"poverty to Paris through the pursuit of gold, to this day undiscovered, in\n",
|
||
"Louisiana. In these years also the great South Sea Bubble spread financial\n",
|
||
"devastation in Britain.Later there was more. In the United States in the\n",
|
||
"nineteenth century there was a speculative splurge every twenty or thirty years.\n",
|
||
"This was already a tradition, for the colonies, north and south, had\n",
|
||
"experimented at no slight eventual cost with currency issues that had no visible\n",
|
||
"backing. They did well until it was observed that there was nothing there. The\n",
|
||
"Revolution was paid for with Continental notes, giving permanence to the phrase\n",
|
||
"\"not worth a Continental.\" In the years following the war of 1812–14, there was\n",
|
||
"a major real estate boom; in the 1830s came wild speculation in canal and\n",
|
||
"turnpike investment—internal improvements, they were termed. Along with this\n",
|
||
"went issues of bank notes unbacked by anything of value and issued by anyone\n",
|
||
"able to hire a building larger than that of the local blacksmith. This came\n",
|
||
"powerfully to an end in 1837. In the 1850s came another boom and collapse, and\n",
|
||
"in those years a New England bank, in a part of the country more cautious than\n",
|
||
"most, closed down. It had $500,000 in notes outstanding and assets to cover them\n",
|
||
"of $86.48. After the Civil War came the railroad boom and a particularly painful\n",
|
||
"collapse in 1873. Another boom came to an equally dramatic end in 1907, but the\n",
|
||
"big New York banks were able, this time, to limit the damage. Earlier a\n",
|
||
"considerable flow of British funds had fueled the American speculation, notably\n",
|
||
"that just mentioned in railroads. There was also a renewed British involvement\n",
|
||
"in South America, the South Sea Bubble now forgotten. The greatly distinguished\n",
|
||
"Baring Brothers had to be rescued by the Bank of England from bankruptcy\n",
|
||
"occasioned by its loans to Argentina. This is currently interesting, for in the\n",
|
||
"1990s Barings was caught up in the more or less incredible operations of one of\n",
|
||
"its minor minions in Singapore. This time there was no rescue; Barings, for all\n",
|
||
"public purposes, disappeared.If we do now have a downturn—what is called a day\n",
|
||
"of reckoning—some things can, indeed, be foreseen. By some estimates a quarter\n",
|
||
"of all Americans, directly or indirectly, are in the stock market. Were there a\n",
|
||
"bad slump, it would limit their expenditures, especially of durable goods, and\n",
|
||
"put pressure on their very large credit card debt. The result would be a\n",
|
||
"generally adverse effect on the economy. This would not be as painful as the\n",
|
||
"aftereffects of 1929; then banks were fragile and without deposit insurance,\n",
|
||
"farm markets were important and especially vulnerable, there was no cushioning\n",
|
||
"effect from unemployment compensation, welfare payments and Social Security. All\n",
|
||
"this is better now. But there could be a recession; that would be normal. There\n",
|
||
"would also be, we may be certain, the traditional reassuring words from\n",
|
||
"Washington. Always when markets are in trouble, the phrases are the same: \"The\n",
|
||
"economic situation is fundamentally sound\" or simply \"The fundamentals are\n",
|
||
"good.\" All who hear these words should know that something is wrong. Once more I\n",
|
||
"do not predict and tell only what the past so vividly tells us. I offer a final\n",
|
||
"word on this book. It was published in that spring of 1955 to an appreciative\n",
|
||
"audience. There was a brief appearance on the best-seller lists; I looked with\n",
|
||
"pleasure at the bookstore windows. On my frequent visits to New York, I was\n",
|
||
"distressed, however, to see no sign of it in a small bookshop on the ramp\n",
|
||
"leading down to the planes in the old La Guardia terminal. One night I stopped\n",
|
||
"in to inspect the shelves. The lady in charge finally noticed me and asked what\n",
|
||
"I sought. Somewhat embarrassed, I passed over the name of the author and said it\n",
|
||
"was a work called The Great Crash. \"Not a book you could sell in an airport,\"\n",
|
||
"she responded firmly. A Note on SourcesIn recent times numerous authors\n",
|
||
"and publishers have come to suppose that readers are offended by footnotes. I\n",
|
||
"have no desire to offend or even in the slightest way to discourage any solvent\n",
|
||
"customer, but I regard this supposition as silly. No literate person can\n",
|
||
"possibly be disturbed by a little small type at the bottom of a page, and\n",
|
||
"everyone, professional and lay reader alike, needs to know on occasion the\n",
|
||
"credentials of a fact. Footnotes also provide an exceedingly good index of the\n",
|
||
"care with which a subject has been researched.However, there is also a line\n",
|
||
"between adequacy and pedantry. In this book, where I have drawn on public\n",
|
||
"documents, books, magazine articles, or special sources of any kind I have\n",
|
||
"indicated the source. However, much of the story of 1929 is to be found in the\n",
|
||
"general and financial press of the time. Systematic citation of these sources\n",
|
||
"would involve endless references to the same papers. This I have not done. It\n",
|
||
"means in general that if no source is given, the reader can assume it was in the\n",
|
||
"New York Times, the Wall Street Journal, and the other papers of general\n",
|
||
"circulation of the day. CHAPTER I\"Vision and Boundless Hope and\n",
|
||
"Optimism\"ON DECEMBER 4, 1928, President Coolidge sent his last message on the\n",
|
||
"state of the Union to the reconvening Congress. Even the most melancholy\n",
|
||
"congressman must have found reassurance in his words. \"No Congress of the United\n",
|
||
"States ever assembled, on surveying the state of the Union, has met with a more\n",
|
||
"pleasing prospect than that which appears at the present time. In the domestic\n",
|
||
"field there is tranquility and contentment ... and the highest record of years\n",
|
||
"of prosperity. In the foreign field there is peace, the goodwill which comes\n",
|
||
"from mutual understanding ...\" He told the legislators that they and the country\n",
|
||
"might \"regard the present with satisfaction and anticipate the future with\n",
|
||
"optimism.\" And breaking sharply with the most ancient of our political\n",
|
||
"conventions, he omitted to attribute this well-being to the excellence of the\n",
|
||
"administration which he headed. \"The main source of these unexampled blessings\n",
|
||
"lies in the integrity and character of the American people.\"A whole generation\n",
|
||
"of historians has assailed Coolidge for the superficial optimism which kept him\n",
|
||
"from seeing that a great storm was brewing at home and also more distantly\n",
|
||
"abroad. This is grossly unfair. It requires neither courage nor prescience to\n",
|
||
"predict disaster. Courage is required of the man who, when things are good, says\n",
|
||
"so. Historians rejoice in crucifying the false prophet of the millennium. They\n",
|
||
"never dwell on the mistake of the man who wrongly predicted Armageddon. There\n",
|
||
"was much that was good about the world of which Coolidge spoke. True, as liberal\n",
|
||
"misanthropes have insisted, the rich were getting richer much faster than the\n",
|
||
"poor were getting less poor. The farmers were unhappy and had been ever since\n",
|
||
"the depression of 1920–21 had cut farm prices sharply but left costs high. Black\n",
|
||
"people in the South and white people in the southern Appalachians continued to\n",
|
||
"dwell in hopeless poverty. Fine old-English houses with high gables, leaded\n",
|
||
"glass, and well-simulated half-timbering were rising in the country club\n",
|
||
"district, while farther in town one encountered the most noisome slums outside\n",
|
||
"the Orient.All this notwithstanding, the twenties in America were a very good\n",
|
||
"time. Production and employment were high and rising. Wages were not going up\n",
|
||
"much, but prices were stable. Although many people were still very poor, more\n",
|
||
"people were comfortably well-off, well-to-do, or rich than ever before. Finally,\n",
|
||
"American capitalism was undoubtedly in a lively phase. Between 1925 and 1929,\n",
|
||
"the number of manufacturing establishments increased from 183,900 to 206,700;\n",
|
||
"the value of their output rose from $60.8 billions to $68.0 billions.1 The\n",
|
||
"Federal Reserve index of industrial production which had averaged only 67 in\n",
|
||
"1921 (1923–25= 100) had risen to 110 by July 1928, and it reached 126 in June\n",
|
||
"1929.2 In 1926, 4,301,000 automobiles were produced. Three years later, in 1929,\n",
|
||
"production had increased by over a million to 5,358,000,3 a figure which\n",
|
||
"compares very decently with the 5,700,000 new car registrations of the opulent\n",
|
||
"year of 1953. Business earnings were rising rapidly, and it was a good time to\n",
|
||
"be in business. Indeed, even the most jaundiced histories of the era concede,\n",
|
||
"tacitly, that times were good, for they nearly all join in taxing Coolidge for\n",
|
||
"his failure to see that they were too good to last.This notion of an iron law of\n",
|
||
"compensation—the notion that the ten good years of the twenties had to be paid\n",
|
||
"for by the ten bad ones of the thirties—is one to which it will be worthwhile to\n",
|
||
"return. IIOne thing in the twenties should have been visible even to\n",
|
||
"Coolidge. It concerned the American people of whose character he had spoken so\n",
|
||
"well. Along with the sterling qualities he praised, they were also displaying an\n",
|
||
"inordinate desire to get rich quickly with a minimum of physical effort. The\n",
|
||
"first striking manifestation of this personality trait was in Florida. There, in\n",
|
||
"the mid-twenties, Miami, Miami Beach, Coral Gables, the East Coast as far north\n",
|
||
"as Palm Beach, and the cities over on the Gulf had been struck by the great\n",
|
||
"Florida real estate boom. The Florida boom contained all of the elements of the\n",
|
||
"classic speculative bubble. There was the indispensable element of substance.\n",
|
||
"Florida had a better winter climate than New York, Chicago, or Minneapolis.\n",
|
||
"Higher incomes and better transportation were making it increasingly accessible\n",
|
||
"to the frost-bound North. The time indeed was coming when the annual flight to\n",
|
||
"the South would be as regular and impressive as the migrations of the Canada\n",
|
||
"Goose.On that indispensable element of fact men and women had proceeded to build\n",
|
||
"a world of speculative make-believe. This is a world inhabited not by people who\n",
|
||
"have to be persuaded to believe but by people who want an excuse to believe. In\n",
|
||
"the case of Florida, they wanted to believe that the whole peninsula would soon\n",
|
||
"be populated by the holiday-makers and the sun-worshippers of a new and\n",
|
||
"remarkably indolent era. So great would be the crush that beaches, bogs, swamps,\n",
|
||
"and common scrubland would all have value. The Florida climate obviously did not\n",
|
||
"insure that this would happen. But it did enable people who wanted to believe it\n",
|
||
"would happen so to believe. However, speculation does not depend entirely on the\n",
|
||
"capacity for self-delusion. In Florida land was divided into building lots and\n",
|
||
"sold for a 10 per cent down payment. Palpably, much of the unlovely terrain that\n",
|
||
"thus changed hands was as repugnant to the people who bought it as to the\n",
|
||
"passer-by. The buyers did not expect to live on it; it was not easy to suppose\n",
|
||
"that anyone ever would. But these were academic considerations. The reality was\n",
|
||
"that this dubious asset was gaining in value by the day and could be sold at a\n",
|
||
"handsome profit in a fortnight. It is another feature of the speculative mood\n",
|
||
"that, as time passes, the tendency to look beyond the simple fact of increasing\n",
|
||
"values to the reasons on which it depends greatly diminishes. And there is no\n",
|
||
"reason why anyone should do so as long as the supply of people who buy with the\n",
|
||
"expectation of selling at a profit continues to be augmented at a sufficiently\n",
|
||
"rapid rate to keep prices rising.Through 1925 the pursuit of effortless riches\n",
|
||
"brought people to Florida in satisfactorily increasing numbers. More land was\n",
|
||
"subdivided each week. What was loosely called seashore became five, ten, or\n",
|
||
"fifteen miles from the nearest brine. Suburbs became an astonishing distance\n",
|
||
"from town. As the speculation spread northward, an enterprising Bostonian, Mr.\n",
|
||
"Charles Ponzi, developed a subdivision \"near Jacksonville.\" It was approximately\n",
|
||
"sixty-five miles west of the city. (In other respects Ponzi believed in good,\n",
|
||
"compact neighborhoods; he sold twenty-three lots to the acre.) In instances\n",
|
||
"where the subdivision was close to town, as in the case of Manhattan Estates,\n",
|
||
"which were \"not more than three fourths of a mile from the prosperous and fast-\n",
|
||
"growing city of Nettie,\" the city, as was so of Nettie, did not exist. The\n",
|
||
"congestion of traffic into the state became so severe that in the autumn of 1925\n",
|
||
"the railroads were forced to proclaim an embargo on less essential freight,\n",
|
||
"which included building materials for developing the subdivisions. Values rose\n",
|
||
"wonderfully. Within forty miles of Miami \"inside\" lots sold at from $8000 to\n",
|
||
"$20,000; waterfront lots brought from $15,000 to $25,000, and more or less bona\n",
|
||
"fide seashore sites brought $20,000 to $75,000.4 However, in the spring of 1926,\n",
|
||
"the supply of new buyers, so essential to the reality of increasing prices,\n",
|
||
"began to fail. As 1928 and 1929 were to show, the momentum built up by a good\n",
|
||
"boom is not dissipated in a moment. For a while in 1926 the increasing eloquence\n",
|
||
"of the promoters offset the diminishing supply of prospects. (Even the cathedral\n",
|
||
"voice of William Jennings Bryan, which once had thundered against the cross of\n",
|
||
"gold, had been for a time enlisted in the sorry task of selling swampland.) But\n",
|
||
"this boom was not left to collapse of its own weight. In the autumn of 1926, two\n",
|
||
"hurricanes showed, in the words of Frederick Lewis Allen, \"what a Soothing\n",
|
||
"Tropic Wind could do when it got a running start from the West Indies.\" 5 The\n",
|
||
"worst of these winds, on September 18, 1926, killed four hundred people, tore\n",
|
||
"the roofs from thousands of houses, and piled tons of water and a number of\n",
|
||
"elegant yachts into the streets of Miami. There was agreement that the storm had\n",
|
||
"caused a healthy breathing spell in the boom, although its resumption was\n",
|
||
"predicted daily. In the Wall Street Journal of October 8, 1926, one Peter O.\n",
|
||
"Knight, an official of the Seaboard Air Line and a sincere believer in the\n",
|
||
"future of Florida, acknowledged that some seventeen or eighteen thousand people\n",
|
||
"were in need of assistance. But he added: \"The same Florida is still there with\n",
|
||
"its magnificent resources, its wonderful climate, and its geographical position.\n",
|
||
"It is the Riviera of America.\" He expressed concern that the solicitation of Red\n",
|
||
"Cross funds for hurricane relief would \"do more damage permanently to Florida\n",
|
||
"than would be offset by the funds received.\"6 This reluctance to concede that\n",
|
||
"the end has come is also in accordance with the classic pattern. The end had\n",
|
||
"come in Florida. In 1925 bank clearings in Miami were $1,066,- 528,000; by 1928\n",
|
||
"they were down to $143,364,000.7 Farmers who had sold their land at a handsome\n",
|
||
"price and had condemned themselves as it later sold for double, treble,\n",
|
||
"quadruple the original price, now on occasion got it back through a whole chain\n",
|
||
"of subsequent defaults. Sometimes it was equipped with eloquently named streets\n",
|
||
"and with sidewalks, street lamps, and taxes and assessments amounting to several\n",
|
||
"times its current value.The Florida boom was the first indication of the mood of\n",
|
||
"the twenties and the conviction that God intended the American middle class to\n",
|
||
"be rich. But that this mood survived the Florida collapse is still more\n",
|
||
"remarkable. It was widely understood that things had gone to pieces in Florida.\n",
|
||
"While the number of speculators was almost certainly small compared with the\n",
|
||
"subsequent participation in the stock market, nearly every community contained a\n",
|
||
"man who was known to have taken \"quite a beating\" in Florida. For a century\n",
|
||
"after the collapse of the South Sea Bubble, Englishmen regarded the most\n",
|
||
"reputable joint stock companies with some suspicion. Even as the Florida boom\n",
|
||
"collapsed, the faith of Americans in quick, effortless enrichment in the stock\n",
|
||
"market was becoming every day more evident. IIIIt is hard to say when\n",
|
||
"the stock market boom of the nineteen-twenties began. There were sound reasons\n",
|
||
"why, during these years, the prices of common stocks should rise. Corporate\n",
|
||
"earnings were good and growing. The prospect seemed benign. In the early\n",
|
||
"twenties stock prices were low and yields favorable.In the last six months of\n",
|
||
"1924, the prices of securities began to rise, and the increase was continued and\n",
|
||
"extended through 1925. Thus at the end of May 1924, the New York Times average\n",
|
||
"of the prices of twenty-five industrial stocks was 106; by the end of the year\n",
|
||
"it was 134.8 By December 31, 1925, it had gained very nearly another 50 points\n",
|
||
"and stood at 181. The advance through 1925 was remarkably steady; there were\n",
|
||
"only a couple of months when values did not show a net gain. During 1926 there\n",
|
||
"was something of a setback. Business was off a little in the early part of that\n",
|
||
"year; it was thought by many that values the year before had risen unreasonably.\n",
|
||
"February brought a sharp fall in the market, and March a rather abrupt collapse.\n",
|
||
"The Times industrials went down from 181 at the beginning of the year to 172 at\n",
|
||
"the end of February, and then dropped by nearly 30 points to 143 at the end of\n",
|
||
"March. However, in April the market steadied and renewed its advance. Another\n",
|
||
"mild setback occurred in October, just after the hurricane blew away the\n",
|
||
"vestiges of the Florida boom, but again recovery was prompt. At the end of the\n",
|
||
"year values were about where they had been at the beginning.In 1927 the increase\n",
|
||
"began in earnest. Day after day and month after month the price of stocks went\n",
|
||
"up. The gains by later standards were not large, but they had an aspect of great\n",
|
||
"reliability. Again in only two months in 1927 did the averages fail to show an\n",
|
||
"increase. On May 20, when Lindbergh took off from Roosevelt Field and headed for\n",
|
||
"Paris, a fair number of citizens were unaware of the event. The market, which\n",
|
||
"that day was registering another of its small but solid gains, had by then\n",
|
||
"acquired a faithful band of devotees who spared no attention for more celestial\n",
|
||
"matters.In the summer of 1927 Henry Ford rang down the curtain on the immortal\n",
|
||
"Model T and closed his plant to prepare for Model A. The Federal Reserve index\n",
|
||
"of industrial production receded, presumably as a result of the Ford shutdown,\n",
|
||
"and there was general talk of depression. The effect on the market was\n",
|
||
"imperceptible. At the end of the year, by which time production had also turned\n",
|
||
"up again, the Times industrials had reached 245, a net gain of 69 points for the\n",
|
||
"year. The year 1927 is historic from another point of view in the lore of the\n",
|
||
"stock market. According to a long accepted doctrine, it was in this year that\n",
|
||
"the seeds of the eventual disaster were sown. The responsibility rests with an\n",
|
||
"act of generous but ill-advised internationalism. Some—including Mr. Hoover—have\n",
|
||
"thought it almost disloyal, although in those days accusations of treason were\n",
|
||
"still made with some caution.In 1925, under the aegis of the then Chancellor of\n",
|
||
"the Exchequer, Mr. Winston Churchill, Britain returned to the gold standard at\n",
|
||
"the old or pre-World War I relationship between gold, dollars, and the pound.\n",
|
||
"There is no doubt that Churchill was more impressed by the grandeur of the\n",
|
||
"traditional, or $4.86, pound than by the more subtle consequences of\n",
|
||
"overvaluation, which he is widely assumed not to have understood. The\n",
|
||
"consequences, nonetheless, were real and severe. Customers of Britain had now to\n",
|
||
"use these costly pounds to buy goods at prices that still reflected wartime\n",
|
||
"inflation. Britain was, accordingly, an unattractive place for foreigners to\n",
|
||
"buy. For the same reason it was an easy place in which to sell. In 1925 began\n",
|
||
"the long series of exchange crises which, like the lions in Trafalgar Square and\n",
|
||
"the street walkers in Piccadilly, are now an established part of the British\n",
|
||
"scene. There were also unpleasant domestic consequences; the bad market for coal\n",
|
||
"and the effort to reduce costs and prices to meet world competition led to the\n",
|
||
"general strike in 1926.Then, as since, gold when it escaped from Britain or\n",
|
||
"Europe came to the United States. This might be discouraged if prices of goods\n",
|
||
"were high and interest rates were low in this country. (The United States would\n",
|
||
"be a poor place in which to buy and invest.) In the spring of 1927, three august\n",
|
||
"pilgrims—Montagu Norman, the Governor of the Bank of England, the durable\n",
|
||
"Hjalmar Schacht, then Governor of the Reichsbank, and Charles Rist, the Deputy\n",
|
||
"Governor of the Bank of France—came to the United States to urge an easy money\n",
|
||
"policy. (They had previously pled with success for a roughly similar policy in\n",
|
||
"1925.) The Federal Reserve obliged. The rediscount rate of the New York Federal\n",
|
||
"Reserve Bank was cut from 4 to 3.5 per cent. Government securities were\n",
|
||
"purchased in considerable volume with the mathematical consequence of leaving\n",
|
||
"the banks and individuals who had sold them with money to spare. Adolph C.\n",
|
||
"Miller, a dissenting member of the Federal Reserve Board, subsequently described\n",
|
||
"this as \"the greatest and boldest operation ever undertaken by the Federal\n",
|
||
"Reserve System, and...[it] resulted in one of the most costly errors committed\n",
|
||
"by it or any other banking system in the last 75 years!\"9 The funds that the\n",
|
||
"Federal Reserve made available were either invested in common stocks or (and\n",
|
||
"more important) they became available to help finance the purchase of common\n",
|
||
"stocks by others. So provided with funds, people rushed into the market. Perhaps\n",
|
||
"the most widely read of all the interpretations of the period, that of Professor\n",
|
||
"Lionel Robbins of the London School of Economics, concludes: \"From that date,\n",
|
||
"according to all the evidence, the situation got completely out of control.\"10\n",
|
||
"This view that the action of the Federal Reserve authorities in 1927 was\n",
|
||
"responsible for the speculation and collapse which followed has never been\n",
|
||
"seriously shaken. There are reasons why it is attractive. It is simple, and it\n",
|
||
"exonerates both the American people and their economic system from any\n",
|
||
"substantial blame. The danger of being guided by foreigners is well known, and\n",
|
||
"Norman and Schacht had some special reputation for sinister motives.Yet the\n",
|
||
"explanation obviously assumes that people will always speculate if only they can\n",
|
||
"get the money to finance it. Nothing could be farther from the case. There were\n",
|
||
"times before and there have been long periods since when credit was plentiful\n",
|
||
"and cheap—far cheaper than in 1927–29—and when speculation was negligible. Nor,\n",
|
||
"as we shall see later, was speculation out of control after 1927, except that it\n",
|
||
"was beyond the reach of men who did not want in the least to control it. The\n",
|
||
"explanation is a tribute only to a recurrent preference, in economic matters,\n",
|
||
"for formidable nonsense. IVUntil the beginning of 1928, even a man of\n",
|
||
"conservative mind could believe that the prices of common stock were catching up\n",
|
||
"with the increase in corporation earnings, the prospect for further increases,\n",
|
||
"the peace and tranquility of the times, and the certainty that the\n",
|
||
"Administration then firmly in power in Washington would take no more than\n",
|
||
"necessary of any earnings in taxes. Early in 1928, the nature of the boom\n",
|
||
"changed. The mass escape into make-believe, so much a part of the true\n",
|
||
"speculative orgy, started in earnest. It was still necessary to reassure those\n",
|
||
"who required some tie, however tenuous, to reality. And, as will be seen\n",
|
||
"presently, this process of reassurance—of inventing the industrial equivalents\n",
|
||
"of the Florida climate—eventually achieved the status of a profession. However,\n",
|
||
"the time had come, as in all periods of speculation, when men sought not to be\n",
|
||
"persuaded of the reality of things but to find excuses for escaping into the new\n",
|
||
"world of fantasy. There were many indications by 1928 that this phase had come.\n",
|
||
"Most obvious was the behavior of the market. While the winter months of 1928\n",
|
||
"were rather quiet, thereafter the market began to rise, not by slow, steady\n",
|
||
"steps, but by great vaulting leaps. On occasion it also came down the same way,\n",
|
||
"only to recover and go higher again. In March 1928 the industrial average rose\n",
|
||
"nearly 25 points. News of the boiling market was frequently on the front page.\n",
|
||
"Individual issues sometimes made gains of 10, 15, and 20 points in a single\n",
|
||
"day's trading. On March 12, Radio, in many respects the speculative symbol of\n",
|
||
"the time, gained 18 points. On the following day it opened 22 points above the\n",
|
||
"previous close. Then it lost 20 points on the announcement that the behavior of\n",
|
||
"the trading in the stock was being investigated by the Exchange, gained 15\n",
|
||
"points, and fell off 9.11 A few days later, on a strong market, it made another\n",
|
||
"18-point gain.The March boom also celebrated, beyond anything theretofore, the\n",
|
||
"operations of the big professional traders. The lore of competitive markets\n",
|
||
"pictures the stock exchange as the most impersonal of markets. No doctrine is\n",
|
||
"more jealously guarded by the prophets and defenders of the Stock Exchange. \"The\n",
|
||
"Exchange is a market place where prices reflect the basic law of supply and\n",
|
||
"demand,\" the New York Stock Exchange says firmly of itself.12 Yet even the most\n",
|
||
"devout Wall Streeter allows himself on occasion to believe that more personal\n",
|
||
"influences have a hand in his destiny. Somewhere around there are big men who\n",
|
||
"put stocks up and put them down. As the boom developed, the big men became more\n",
|
||
"and more omnipotent in the popular or at least in the speculative view. In\n",
|
||
"March, according to this view, the big men decided to put the market up, and\n",
|
||
"even some serious scholars have been inclined to think that a concerted move\n",
|
||
"catalyzed this upsurge. If so, the important figure was John J. Raskob. Raskob\n",
|
||
"had impressive associations. He was a director of General Motors, an ally of the\n",
|
||
"Du Ponts and soon to be Chairman of the Democratic National Committee by choice\n",
|
||
"of Al Smith. A contemporary student of the market, Professor Charles Amos Dice\n",
|
||
"of the Ohio State University, thought this latter appointment a particular\n",
|
||
"indication of the new prestige of Wall Street and the esteem in which it was\n",
|
||
"held by the American people. \"Today,\" he observed, \"the shrewd, worldly-wise\n",
|
||
"candidate of one of the great political parties chooses one of the outstanding\n",
|
||
"operators in the stock market ... as a goodwill creator and popular vote\n",
|
||
"getter.\" 13On March 23, 1928, on taking ship for Europe, Raskob spoke favorably\n",
|
||
"of prospects for automobile sales for the rest of the year and of the share in\n",
|
||
"the business that General Motors would have. He may also have suggested—the\n",
|
||
"evidence is not entirely clear—that G.M. stock should be selling at not less\n",
|
||
"than twelve times earnings. This would have meant a price of 225 as compared\n",
|
||
"with a current quotation of about 187. Such, as the Times put it, was \"the magic\n",
|
||
"of his name\" that Mr. Raskob's \"temperate bit of optimism\" sent the market into\n",
|
||
"a boiling fury. On March 24, a Saturday, General Motors gained nearly 5 points,\n",
|
||
"and the Monday following it went to 199. The surge in General Motors, meanwhile,\n",
|
||
"set off a great burst of trading elsewhere in the list. Among the others who\n",
|
||
"were assumed to have put their strength behind the market that spring was\n",
|
||
"William Crapo Durant. Durant was the organizer of General Motors, whom Raskob\n",
|
||
"and the Du Ponts had thrown out of the company in 1920. After a further\n",
|
||
"adventure in the auto business, he had turned to full-time speculation in the\n",
|
||
"stock market. The seven Fisher brothers were also believed to be influential.\n",
|
||
"They too were General Motors alumni and had come to Wall Street with the great\n",
|
||
"fortune they had realized from the sale of the Fisher-body plants. Still another\n",
|
||
"was Arthur W. Cutten, the Canadian-born grain speculator who had recently\n",
|
||
"shifted his market operations to Wall Street from the Chicago Board of Trade. As\n",
|
||
"a market operator, Cutten surmounted substantial personal handicaps. He was very\n",
|
||
"hard of hearing, and some years later, before a congressional committee, even\n",
|
||
"his own counsel conceded that his memory was very defective.Observing this group\n",
|
||
"as a whole Professor Dice was especially struck by their \"vision for the future\n",
|
||
"and boundless hope and optimism.\" He noted that \"they did not come into the\n",
|
||
"market hampered by the heavy armor of tradition.\" In recounting their effect on\n",
|
||
"the market, Professor Dice obviously found the English language verging on\n",
|
||
"inadequacy. \"Led by these mighty knights of the automobile industry, the steel\n",
|
||
"industry, the radio industry...\" he said, \"and finally joined, in despair, by\n",
|
||
"many professional traders who, after much sack-cloth and ashes, had caught the\n",
|
||
"vision of progress, the Coolidge market had gone forward like the phalanxes of\n",
|
||
"Cyrus, parasang upon parasang and again parasang upon parasang...\"14 VIn\n",
|
||
"June of 1928 the market retreated a parasang or two—in fact, the losses during\n",
|
||
"the first three weeks were almost as great as the March gains. June 12, a day of\n",
|
||
"particularly heavy losses, was a landmark. For a year or more, men of vision had\n",
|
||
"been saying that the day might come when five million shares would be traded on\n",
|
||
"the New York Stock Exchange. Once this had been only a wild conversational\n",
|
||
"gambit, but for some time it had shown signs of being overtaken by the reality.\n",
|
||
"On March 12, the volume of trading had reached 3,875,910 shares, an all-time\n",
|
||
"high. By the end of the month such a volume had become commonplace. On March 27,\n",
|
||
"4,790,270 shares were traded. Then on June 12, 5,052, 790 shares changed hands.\n",
|
||
"The ticker also fell nearly two hours behind the market; Radio dropped 23\n",
|
||
"points, and a New York paper began its accounts of the day's events, \"Wall\n",
|
||
"Street's bull market collapsed yesterday with a detonation heard round the\n",
|
||
"world.\"The announcement of the death of the bull market was as premature as any\n",
|
||
"since that of the death of Mark Twain. In July there was a small net gain, and\n",
|
||
"in August a strong upsurge. Thereafter not even the approach of the election\n",
|
||
"caused serious hesitation. People remained unperturbed when, on September 17,\n",
|
||
"Roger W. Babson told an audience in Wellesley, Massachusetts, that \"if Smith\n",
|
||
"should be elected with a Democratic Congress we are almost certain to have a\n",
|
||
"resulting business depression in 1929.\" He also said that \"the election of\n",
|
||
"Hoover and a Republican Congress should result in continued prosperity for\n",
|
||
"1929,\" and it may have been that the public knew it would be Hoover. In any\n",
|
||
"case, during the same month reassurance came from still higher authority. Andrew\n",
|
||
"W. Mellon said, \"There is no cause for worry. The high tide of prosperity will\n",
|
||
"continue.\"Mr. Mellon did not know. Neither did any of the other public figures\n",
|
||
"who then, as since, made similar statements. These are not forecasts; it is not\n",
|
||
"to be supposed that the men who make them are privileged to look farther into\n",
|
||
"the future than the rest. Mr. Mellon was participating in a ritual which, in our\n",
|
||
"society, is thought to be of great value for influencing the course of the\n",
|
||
"business cycle. By affirming solemnly that prosperity will continue, it is\n",
|
||
"believed, one can help insure that prosperity will in fact continue. Especially\n",
|
||
"among businessmen the faith in the efficiency of such incantation is very great.\n",
|
||
"VIHoover was elected in a landslide. This, were the speculators privy to Mr.\n",
|
||
"Hoover's mind, should have caused a heavy fall in the market. In his memoirs Mr.\n",
|
||
"Hoover states that as early as 1925 he became concerned over the \"growing tide\n",
|
||
"of speculation.\" 15 During the months and years that followed this concern\n",
|
||
"gradually changed to alarm, and then to something only slightly less than a\n",
|
||
"premonition of total disaster. \"There are crimes,\" Mr. Hoover said of\n",
|
||
"speculation, \"far worse than murder for which men should be reviled and\n",
|
||
"punished.\"16 As Secretary of Commerce he had sought nothing so much as to get\n",
|
||
"the market under control.Mr. Hoover's attitude toward the market was, however,\n",
|
||
"an exceptionally well-kept secret. People did not know of his efforts, uniformly\n",
|
||
"frustrated by Coolidge and the Federal Reserve Board, to translate his thoughts\n",
|
||
"into action. The news of his election, so far from causing a panic, set off the\n",
|
||
"greatest increase in buying to date. On November 7, the day after the election,\n",
|
||
"there was a \"victory boom,\" and the market leaders climbed 5 to 15 points.\n",
|
||
"Volume reached 4,894,670 shares, or only a little less than the all-time record\n",
|
||
"of June 12, and this new level was reached on a rising, not a falling market. On\n",
|
||
"November 16, a further wave of buying hit the market. An astonishing 6,641,250\n",
|
||
"shares changed hands—far above the previous record. The Times industrial\n",
|
||
"averages made a net gain of 4½ points on the day's trading—then considered an\n",
|
||
"impressive advance. Apart from the afterglow of the election, there was nothing\n",
|
||
"particular to incite this enthusiasm. The headlines of the day told only of the\n",
|
||
"sinking of the steamship Vestris and the epic achievements of the officers and\n",
|
||
"crew in shouldering aside the women and children and saving their own lives.\n",
|
||
"November 20 was another huge day. Trading—6,503,230 shares—was fractionally\n",
|
||
"smaller than on the sixteenth, but by common agreement it was much more frantic.\n",
|
||
"The following morning the Times observed that \"for cyclonic violence yesterday's\n",
|
||
"stock market has never been exceeded in the history of Wall Street.\" December\n",
|
||
"was not so good. Early in the month there was a bad break, and, on December 8,\n",
|
||
"Radio fell a ghastly 72 points in one day. However, the market steadied and then\n",
|
||
"came back. Over the whole year of 1928 the Times industrial average gained 86\n",
|
||
"points, or from 245 to 331. During the year Radio went from 85 to 420 (it had\n",
|
||
"never paid a dividend); Du Pont went from 310 to 525; Montgomery Ward from 117\n",
|
||
"to 440; Wright Aeronautic from 69 to 289.17 During the year 920,550,032 shares\n",
|
||
"were traded on the New York Stock Exchange, as compared with a record-breaking\n",
|
||
"576,990,875 in 1927.18 But there was still another and even more significant\n",
|
||
"index of what was happening in the market. That was the phenomenal increase in\n",
|
||
"trading on margin. VIIAs noted, at some point in the growth of a boom\n",
|
||
"all aspects of property ownership become irrelevant except the prospect for an\n",
|
||
"early rise in price. Income from the property, or enjoyment of its use, or even\n",
|
||
"its long-run worth is now academic. As in the case of the more repulsive Florida\n",
|
||
"lots, these usufructs may be non-existent or even negative. What is important is\n",
|
||
"that tomorrow or next week market values will rise—as they did yesterday or last\n",
|
||
"week—and a profit can be realized.It follows that the only reward to ownership\n",
|
||
"in which the boomtime owner has an interest is the increase in values. Could the\n",
|
||
"right to the increased value be somehow divorced from the other and now\n",
|
||
"unimportant fruits of possession and also from as many as possible of the\n",
|
||
"burdens of ownership, this would be much welcomed by the speculator. Such an\n",
|
||
"arrangement would enable him to concentrate on speculation which, after all, is\n",
|
||
"the business of a speculator.Such is the genius of capitalism that where a real\n",
|
||
"demand exists it does not go long unfilled. In all great speculative orgies\n",
|
||
"devices have appeared to enable the speculator so to concentrate on his\n",
|
||
"business. In the Florida boom the trading was in \"binders.\" Not the land itself\n",
|
||
"but the right to buy the land at a stated price was traded. This right to\n",
|
||
"buy—which was obtained by a down payment of 10 per cent of the purchase\n",
|
||
"price—could be sold. It thus conferred on the speculators the full benefit of\n",
|
||
"the increase in values. After the value of the lot had risen he could resell the\n",
|
||
"binder for what he had paid plus the full amount of the increase in price. The\n",
|
||
"worst of the burdens of ownership, whether of land or any other asset, is the\n",
|
||
"need to put up the cash represented by the purchase price. The use of the binder\n",
|
||
"cut this burden by 90 per cent—or it multiplied tenfold the amount of acreage\n",
|
||
"from which the speculator could harvest an increase in value. The buyer happily\n",
|
||
"gave up the other advantages of ownership. These included the current income of\n",
|
||
"which, invariably, there was none and the prospect of permanent use in which he\n",
|
||
"had not the slightest interest.The stock market also has its design for\n",
|
||
"concentrating the speculative energies of the speculator, and, as might be\n",
|
||
"expected, it improves substantially on the crudities of the real estate market.\n",
|
||
"In the stock market the buyer of securities on margin gets full title to his\n",
|
||
"property in an unconditional sale. But he rids himself of the most grievous\n",
|
||
"burden of ownership—that of putting up the purchase price—by leaving his\n",
|
||
"securities with his broker as collateral for the loan that paid for them. The\n",
|
||
"buyer again gets the full benefit of any increase in value—the price of the\n",
|
||
"securities goes up, but the loan that bought them does not. In the stock market\n",
|
||
"the speculative buyer also gets the earnings of the securities he purchased.\n",
|
||
"However, in the days of this history the earnings were almost invariably less\n",
|
||
"than the interest that was paid on the loan. Often they were much less. Yields\n",
|
||
"on securities regularly ranged from nothing to 1 or 2 per cent. Interest on the\n",
|
||
"loans that carried them was often 8, 10, or more per cent. The speculator was\n",
|
||
"willing to pay to divest himself of all of the usufructs of security ownership\n",
|
||
"except the chance for a capital gain.The machinery by which Wall Street\n",
|
||
"separates the opportunity to speculate from the unwanted returns and burdens of\n",
|
||
"ownership is ingenious, precise, and almost beautiful. Banks supply funds to\n",
|
||
"brokers, brokers to customers, and the collateral goes back to banks in a smooth\n",
|
||
"and all but automatic flow. Margins—the cash which the speculator must supply in\n",
|
||
"addition to the securities to protect the loan and which he must augment if the\n",
|
||
"value of the collateral securities should fall and so lower the protection they\n",
|
||
"provide—are effortlessly calculated and watched. The interest rate moves quickly\n",
|
||
"and easily to keep the supply of funds adjusted to the demand. Wall Street,\n",
|
||
"however, has never been able to express its pride in these arrangements. They\n",
|
||
"are admirable and even wonderful only in relation to the purpose they serve. The\n",
|
||
"purpose is to accommodate the speculator and facilitate speculation. But the\n",
|
||
"purposes cannot be admitted. If Wall Street confessed this purpose, many\n",
|
||
"thousands of moral men and women would have no choice but to condemn it for\n",
|
||
"nurturing an evil thing and call for reform. Margin trading must be defended not\n",
|
||
"on the grounds that it efficiently and ingeniously assists the speculator, but\n",
|
||
"that it encourages the extra trading which changes a thin and anemic market into\n",
|
||
"a thick and healthy one. At best this is a dull by-product and a dubious one.\n",
|
||
"Wall Street, in these matters, is like a lovely and accomplished woman who must\n",
|
||
"wear black cotton stockings, heavy woolen underwear, and parade her knowledge as\n",
|
||
"a cook because, unhappily, her supreme accomplishment is as a harlot. However,\n",
|
||
"even the most circumspect friend of the market would concede that the volume of\n",
|
||
"brokers' loans—of loans collateraled by the securities purchased on margin—is a\n",
|
||
"good index of the volume of speculation. Measured by this index, the amount of\n",
|
||
"speculation was rising very fast in 1928. Early in the twenties the volume of\n",
|
||
"brokers' loans—because of their liquidity they are often referred to as call\n",
|
||
"loans or loans in the call market—varied from a billion to a billion and a half\n",
|
||
"dollars. By early 1926 they had increased to two and a half billions and\n",
|
||
"remained at about that level for most of the year. During 1927 there was another\n",
|
||
"increase of about a billion dollars, and at the end of the year they reached\n",
|
||
"$3,480,780,000. This was an incredible sum, but it was only the beginning. In\n",
|
||
"the two dull winter months of 1928 there was a small decline and then expansion\n",
|
||
"began in earnest. Brokers' loans reached four billion on the first of June 1928,\n",
|
||
"five billion on the first of November, and by the end of the year they were well\n",
|
||
"along to six billion.19 Never had there been anything like it before. People\n",
|
||
"were swarming to buy stocks on margin—in other words, to have the increase in\n",
|
||
"price without the costs of ownership. This cost was being assumed, in the first\n",
|
||
"instance, by the New York banks, but they, in turn, were rapidly becoming the\n",
|
||
"agents for lenders the country over and even the world around. There is no\n",
|
||
"mystery as to why so many wished to lend so much in New York. One of the\n",
|
||
"paradoxes of speculation in securities is that the loans that underwrite it are\n",
|
||
"among the safest of all investments. They are protected by stocks which under\n",
|
||
"all ordinary circumstances are instantly salable, and by a cash margin as well.\n",
|
||
"The money, as noted, can be retrieved on demand. At the beginning of 1928 this\n",
|
||
"admirably liquid and exceptionally secure outlet for non-risk capital was paying\n",
|
||
"around 5 per cent. While 5 per cent is an excellent gilt-edged return, the rate\n",
|
||
"rose steadily through 1928, and during the last week of the year it reached 12\n",
|
||
"per cent. This was still with complete safety.In Montreal, London, Shanghai, and\n",
|
||
"Hong Kong there was talk of these rates. Everywhere men of means told themselves\n",
|
||
"that 12 per cent was 12 per cent. A great river of gold began to converge on\n",
|
||
"Wall Street, all of it to help Americans hold common stock on margin.\n",
|
||
"Corporations also found these rates attractive. At 12 per cent Wall Street might\n",
|
||
"even provide a more profitable use for the working capital of a company than\n",
|
||
"additional production. A few firms made this decision: instead of trying to\n",
|
||
"produce goods with its manifold headaches and inconveniences, they confined\n",
|
||
"themselves to financing speculation. Many more companies started lending their\n",
|
||
"surplus funds on Wall Street.There were still better ways of making money. In\n",
|
||
"principle, New York banks could borrow money from the Federal Reserve Bank for 5\n",
|
||
"per cent and re-lend it in the call market for 12. In practice they did. This\n",
|
||
"was, possibly, the most profitable arbitrage operation of all time.\n",
|
||
"VIIIHowever, there were many ways of making money in 1928. Never had there been\n",
|
||
"a better time to get rich, and people knew it. 1928, indeed, was the last year\n",
|
||
"in which Americans were buoyant, uninhibited, and utterly happy. It wasn't that\n",
|
||
"1928 was too good to last; it was only that it didn't last.In the January issue\n",
|
||
"of World's Work, Will Payne, after reflecting on the wonders of the year just\n",
|
||
"over, went on to explain the difference between a gambler and an investor. A\n",
|
||
"gambler, he pointed out, wins only because someone else loses. Where it is\n",
|
||
"investment all gain. One investor, he explained, buys General Motors at $100,\n",
|
||
"sells it to another at $150, who sells it to a third at $200. Everyone makes\n",
|
||
"money. As Walter Bagehot once observed: \"All people are most credulous when they\n",
|
||
"are most happy.\"20 CHAPTER IISomething Should Be Done?PURELY IN\n",
|
||
"RETROSPECT it is easy to see how 1929 was destined to be a year to remember.\n",
|
||
"This was not because Mr. Hoover was soon to become President and had inimical\n",
|
||
"intentions toward the market. Those intentions developed at least partly in\n",
|
||
"retrospect. Nor was it because men of wisdom could tell that a depression was\n",
|
||
"overdue. No one, wise or unwise, knew or now knows when depressions are due or\n",
|
||
"overdue.Rather, it was simply that a roaring boom was in progress in the stock\n",
|
||
"market and, like all booms, it had to end. On the first of January of 1929, as a\n",
|
||
"simple matter of probability, it was most likely that the boom would end before\n",
|
||
"the year was out, with a diminishing chance that it would end in any given year\n",
|
||
"thereafter. When prices stopped rising—when the supply of people who were buying\n",
|
||
"for an increase was exhausted—then ownership on margin would become meaningless\n",
|
||
"and everyone would want to sell. The market wouldn't level out; it would fall\n",
|
||
"precipitately.All this being so, the position of the people who had at least\n",
|
||
"nominal responsibility for what was going on was a complex one. One of the\n",
|
||
"oldest puzzles of politics is who is to regulate the regulators. But an equally\n",
|
||
"baffling problem, which has never received the attention it deserves, is who is\n",
|
||
"to make wise those who are required to have wisdom.Some of those in positions of\n",
|
||
"authority wanted the boom to continue. They were making money out of it, and\n",
|
||
"they may have had an intimation of the personal disaster which awaited them when\n",
|
||
"the boom came to an end. But there were also some who saw, however dimly, that a\n",
|
||
"wild speculation was in progress and that something should be done. For these\n",
|
||
"people, however, every proposal to act raised the same intractable problem. The\n",
|
||
"consequences of successful action seemed almost as terrible as the consequences\n",
|
||
"of inaction, and they could be more horrible for those who took the action.A\n",
|
||
"bubble can easily be punctured. But to incise it with a needle so that it\n",
|
||
"subsides gradually is a task of no small delicacy. Among those who sensed what\n",
|
||
"was happening in early 1929, there was some hope but no confidence that the boom\n",
|
||
"could be made to subside. The real choice was between an immediate and\n",
|
||
"deliberately engineered collapse and a more serious disaster later on. Someone\n",
|
||
"would certainly be blamed for the ultimate collapse when it came. There was no\n",
|
||
"question whatever as to who would be blamed should the boom be deliberately\n",
|
||
"deflated. (For nearly a decade the Federal Reserve authorities had been denying\n",
|
||
"their responsibility for the deflation of 1920–21.) The eventual disaster also\n",
|
||
"had the inestimable advantage of allowing a few more days, weeks, or months of\n",
|
||
"life. One may doubt if at any time in early 1929 the problem was ever framed in\n",
|
||
"terms of quite such stark alternatives. But however disguised or evaded, these\n",
|
||
"were the choices which haunted every serious conference on what to do about the\n",
|
||
"market. IIThe men who had responsibility for these ineluctable choices\n",
|
||
"were the President of the United States, the Secretary of the Treasury, the\n",
|
||
"Federal Reserve Board in Washington, and the Governor and Directors of the\n",
|
||
"Federal Reserve Bank of New York. As the most powerful of the Federal Reserve\n",
|
||
"Banks, and the one with the market at its doorstep, the New York bank both had\n",
|
||
"and assumed responsibilities which were not accepted by the other eleven banks\n",
|
||
"of the system. President Coolidge neither knew nor cared what was going on. A\n",
|
||
"few days before leaving office in 1929, he cheerily observed that things were\n",
|
||
"\"absolutely sound\" and that stocks were \"cheap at current prices.\" 1 In earlier\n",
|
||
"years, whenever warned that speculation was getting out of hand, he had\n",
|
||
"comforted himself with the thought that this was the primary responsibility of\n",
|
||
"the Federal Reserve Board.2 The Board was a semi-autonomous body precisely\n",
|
||
"because Congress wanted to protect it from excessive political interference by\n",
|
||
"the Executive.However tender his scruples, President Coolidge could have acted\n",
|
||
"through his Secretary of the Treasury, who served, ex-officio, as a member of\n",
|
||
"the Federal Reserve Board. The Secretary also had the primary responsibility for\n",
|
||
"economic and especially for financial policy. But on this as on other matters of\n",
|
||
"economic policy, the incumbent, Andrew W. Mellon, was a passionate advocate of\n",
|
||
"inaction. The responsibility thus passed to the Federal Reserve Board and the\n",
|
||
"Federal Reserve Banks.The regulation of economic activity is without doubt the\n",
|
||
"most inelegant and unrewarding of public endeavors. Almost everyone is opposed\n",
|
||
"to it in principle; its justification always relies on the unprepossessing case\n",
|
||
"for the lesser evil. Regulation originates in raucous debate in Congress in\n",
|
||
"which the naked interests of pressure groups may at times involve an exposure\n",
|
||
"bordering on the obscene. Promulgation and enforcement of rules and regulations\n",
|
||
"is by grinding bureaucracies which are ceaselessly buffeted by criticism. In\n",
|
||
"recent times it has become obligatory for the regulators at every opportunity to\n",
|
||
"confess their inadequacy, which in any case is all too evident. The great\n",
|
||
"exception to this dreary story is the regulatory activity of the central\n",
|
||
"bank—with us, the Federal Reserve System. Here is regulation of a seemly and\n",
|
||
"becoming sort. No one apologizes for it; men of impeccable conservatism would\n",
|
||
"rise to espouse such regulation were they called upon to do so, which they\n",
|
||
"almost never are. This regulation is not the work of thousands of clerks,\n",
|
||
"statisticians, hearing officers, lawyers, and lesser beings in a teeming office\n",
|
||
"building on the Mall. Rather it emerges in the measured and orderly discussion\n",
|
||
"of men of quiet and dignified mien, each at his accustomed place around a\n",
|
||
"handsome table in a richly paneled and richly draperied room. These men do not\n",
|
||
"issue orders; at most they suggest. Chiefly they move interest rates, buy or\n",
|
||
"sell securities and, in doing so, nudge the economy here and restrain it there.\n",
|
||
"Because the meanings of their actions are not understood by the great majority\n",
|
||
"of the people, they can reasonably be assumed to have superior wisdom. Their\n",
|
||
"actions will on occasion be criticized. More often they will be scrutinized for\n",
|
||
"hidden meaning.Such is the mystique of central banking. Such was the awe-\n",
|
||
"inspiring role in 1929 of the Federal Reserve Board in Washington, the policy-\n",
|
||
"making body which guided and directed the twelve Federal Reserve banks. However,\n",
|
||
"there was a jarring difficulty. The Federal Reserve Board in those times was a\n",
|
||
"body of startling incompetence.For several years, until late in 1927, the\n",
|
||
"Chairman and guiding genius presumptive, was one Daniel R. Crissinger. He had\n",
|
||
"been trained for his task by serving as General Counsel of the Marion Steam\n",
|
||
"Shovel Company of Marion, Ohio. There is no indication that he was an apt\n",
|
||
"student. However, his background seemed satisfactory to another Marion boy,\n",
|
||
"Warren G. Harding, who had brought him to Washington, where he was regarded as a\n",
|
||
"hack politician from Ohio. In 1927 Crissinger was replaced by Roy A. Young, who\n",
|
||
"for eight years had been Governor of the Minneapolis Federal Reserve Bank.\n",
|
||
"Young, a more substantial figure, was undoubtedly aware of what was going on.\n",
|
||
"However, he was a man of caution who sought no fame as a martyr to the broken\n",
|
||
"boom. His colleagues were among the more commonplace of Harding-Coolidge\n",
|
||
"appointees. With one exception—the erstwhile college professor, Adolph C.\n",
|
||
"Miller—they have been conservatively described by Herbert Hoover as\n",
|
||
"\"mediocrities.\"3 The New York Federal Reserve Bank was under more vigorous\n",
|
||
"leadership. For several years, until 1928, its governor had been Benjamin\n",
|
||
"Strong, the first American since Nicholas Biddle to make an important reputation\n",
|
||
"as a central banker. Strong's views were regarded throughout the System with\n",
|
||
"only little less awe than the gold standard. However, in the view of Herbert\n",
|
||
"Hoover—and in this instance Hoover's views are widely shared—Strong, so far from\n",
|
||
"being concerned about the inflation, was the man most responsible for it. It was\n",
|
||
"he who took the lead in 1927 in easing money rates to help the hard-pressed\n",
|
||
"Europeans. For this Mr. Hoover later called him \"a mental annex to Europe.\"4This\n",
|
||
"is unfair. Governor Strong's action was entirely reasonable in the circumstances\n",
|
||
"and, as noted in the last chapter, it takes more to start a speculation than a\n",
|
||
"general ability to borrow money. Still, the New York Federal Reserve Bank, under\n",
|
||
"Governor Strong's leadership may not have been sufficiently perturbed by the\n",
|
||
"speculation a block or two away. Nor was it after Governor Strong died in\n",
|
||
"October 1928 and was replaced by George L. Harrison. A reason, no doubt, was the\n",
|
||
"reassurance provided by people in high places who were themselves speculating\n",
|
||
"heavily. One such was Charles E. Mitchell, the Chairman of the Board of the\n",
|
||
"National City Bank who, on January 1, 1929, became a class A director of the\n",
|
||
"Federal Reserve Bank of New York. The end of the boom would mean the end of\n",
|
||
"Mitchell. He was not a man to expedite his own demise. IIIIn the\n",
|
||
"accepted history of these times, the Federal Reserve authorities are held to be\n",
|
||
"not so much unaware or unwilling as impotent. They would have liked to stop the\n",
|
||
"boom, but they lacked the means. This puts far too elaborate a face on matters.\n",
|
||
"And it largely disguises the real nature of the dilemma which the authorities\n",
|
||
"faced.The classic instruments of control were indeed largely useless. These, as\n",
|
||
"almost every college sophomore knows, are two: open market operations and the\n",
|
||
"manipulation of the rediscount rate. Open market sales of government securities5\n",
|
||
"by the Federal Reserve bring to the vaults of the Reserve Banks the cash which\n",
|
||
"is paid for the securities. There it remains sterile and harmless. Had it stayed\n",
|
||
"in the commercial banks it would have been loaned to the public in multiple\n",
|
||
"volume and particularly in those days to people who were buying common stocks.\n",
|
||
"If such a policy is to succeed, the Federal Reserve System rather obviously must\n",
|
||
"have securities to sell. One of the inestimable blessings of the years of\n",
|
||
"depression, war, and deficit-financing since 1930 is a spacious inventory of\n",
|
||
"government debt in the Reserve Banks. In 1929 the Banks were not so well\n",
|
||
"endowed. At the beginning of 1928, holdings were $617 million. During the first\n",
|
||
"half of the year there were heavy sales as part of an effort to dry up the\n",
|
||
"supply of funds that was feeding the market. Although sales were discontinued in\n",
|
||
"the latter part of the year in the highly erroneous belief that the policy had\n",
|
||
"succeeded and that the boom was under control, they couldn't have been continued\n",
|
||
"much longer in any case. By the end of 1928 the inventory of government\n",
|
||
"securities of the Federal Reserve System amounted to only $228 million. Had\n",
|
||
"these all been dropped into the market, they might possibly have had some\n",
|
||
"effect. But the Board was not given to any such drastic behavior which,\n",
|
||
"incidentally, would also have largely denuded the Reserve Banks of earning\n",
|
||
"assets. Sales were made a few millions at a time in the early months of 1929,\n",
|
||
"but the effect was inconsequential. Moreover, even in following this feeble\n",
|
||
"policy the Board worried lest, in denying funds to the stock market, it might\n",
|
||
"put a pinch on \"legitimate\" business. The Reserve Banks continued to buy\n",
|
||
"acceptances—the security that emerges in the course of financing ordinary non-\n",
|
||
"speculative trade—and, relieved of the need to carry this paper, the commercial\n",
|
||
"banks happily loaned more money in the stock market.The other instrument of\n",
|
||
"Federal Reserve policy was the rediscount rate. This is the rate at which member\n",
|
||
"commercial banks borrow from the Reserve Banks of their district so that they\n",
|
||
"may accommodate more borrowers than their own resources permit. In January 1929,\n",
|
||
"the rediscount rate at the New York Federal Reserve Bank was 5 per cent. The\n",
|
||
"rate on brokers' loans ranged from 6 to 12 per cent. Only a drastic increase\n",
|
||
"would have made it unprofitable for a bank to borrow at the Federal Reserve in\n",
|
||
"order to lend the proceeds, directly or indirectly, in the stock market. Apart\n",
|
||
"from the general aversion to drastic action, such an increase would also have\n",
|
||
"raised rates to ordinary businessmen, consumers, and farmers. In fact, higher\n",
|
||
"interest rates would have been distressing to everyone but the speculator. A man\n",
|
||
"who paid, say, an average of 10 per cent to carry his holdings of Radio through\n",
|
||
"1928 would not have been either deterred or much disturbed had the rate been\n",
|
||
"twice that high. During the same year, he made 500 per cent on the appreciation\n",
|
||
"in the value of his investment. On February 14, 1929, the New York Federal\n",
|
||
"Reserve Bank proposed that the rediscount rate be raised from 5 to 6 per cent to\n",
|
||
"check speculation. The Federal Reserve Board in Washington thought this a\n",
|
||
"meaningless gesture which would only increase rates to business borrowers. A\n",
|
||
"long controversy ensued in which President Hoover sided with the Board against\n",
|
||
"the Bank. The rate was not increased until late in the summer.There was another\n",
|
||
"circumstance which gave the Reserve authorities an admirable excuse for\n",
|
||
"inaction. That was the previously noted flow of funds from corporations and\n",
|
||
"individuals to the market. During 1929, Standard Oil of New Jersey contributed a\n",
|
||
"daily average of $69 million to the call market; Electric Bond and Share\n",
|
||
"averaged over $100 million.6 A few corporations—Cities Service was one—even sold\n",
|
||
"securities and loaned the proceeds in the stock market.7 By early 1929, loans\n",
|
||
"from these non-banking sources were approximately equal to those from the banks.\n",
|
||
"Later they became much greater. The Federal Reserve authorities took for granted\n",
|
||
"that they had no influence whatever over this supply of funds. IVIn\n",
|
||
"fact, the Federal Reserve was helpless only because it wanted to be. Had it been\n",
|
||
"determined to do something, it could for example have asked Congress for\n",
|
||
"authority to halt trading on margin by granting the Board the power to set\n",
|
||
"margin requirements. Margins were not low in 1929; a residue of caution had\n",
|
||
"caused most brokers to require customers to put up in cash 45 to 50 per cent of\n",
|
||
"the value of the stocks they were buying. However, this was all the cash\n",
|
||
"numerous of their customers had. An increase in margins to, say, 75 per cent in\n",
|
||
"January 1929, or even a serious proposal to do so, would have caused many small\n",
|
||
"speculators and quite a few big ones to sell. The boom would have come to a\n",
|
||
"sudden and perhaps spectacular end. (The power to fix margin requirements was\n",
|
||
"eventually given to the Federal Reserve Board by the Securities Exchange Act in\n",
|
||
"1934, a year in which the danger of a revival of speculation about equaled that\n",
|
||
"of a renascence of prohibition.)Actually, not even new legislation, or the\n",
|
||
"threat of it, was needed. In 1929, a robust denunciation of speculators and\n",
|
||
"speculation by someone in high authority and a warning that the market was too\n",
|
||
"high would almost certainly have broken the spell. It would have brought some\n",
|
||
"people back from the world of make-believe. Those who were planning to stay in\n",
|
||
"the market as long as possible but still get out (or go short) in time would\n",
|
||
"have got out or gone short. Their occupational nervousness could readily have\n",
|
||
"been translated into an acute desire to sell. Once the selling started, some\n",
|
||
"more vigorously voiced pessimism could easily have kept it going. The very\n",
|
||
"effectiveness of such a measure was the problem. Of all the weapons in the\n",
|
||
"Federal Reserve arsenal, words were the most unpredictable in their\n",
|
||
"consequences. Their effect might be sudden and terrible. Moreover, these\n",
|
||
"consequences could be attributed with the greatest of precision to the person or\n",
|
||
"persons who uttered the words. Retribution would follow. To the more cautious of\n",
|
||
"the Federal Reserve officials in the early part of 1929 silence seemed literally\n",
|
||
"golden.Yet the boom was continuing. In January the Times industrials gained 30\n",
|
||
"points, more than during the post-election spree in November. Brokers' loans\n",
|
||
"went up a whopping $260 million; on five different days, three of them as the\n",
|
||
"market got off to a boiling start right after New Year's, trading on the New\n",
|
||
"York Stock Exchange exceeded the magic five-million mark. Effective action would\n",
|
||
"be disastrous; yet some action seemed unavoidable. Finally the Board decided to\n",
|
||
"write a letter and issue a press release. It could do no less.On February 2, it\n",
|
||
"addressed the individual Reserve Banks as follows: A member [commercial bank] is\n",
|
||
"not within its reasonable claims for re-discount facilities at its reserve bank\n",
|
||
"when it borrows either for the purpose of making speculative loans or for the\n",
|
||
"purpose of maintaining speculative loans. The board has no disposition to assume\n",
|
||
"authority to interfere with the loan practices of member banks, so long as they\n",
|
||
"do not involve the Federal reserve banks. It has, however, a grave\n",
|
||
"responsibility whenever there is evidence that member banks are maintaining\n",
|
||
"speculative security loans with the aid of Federal reserve credit.8 On February\n",
|
||
"7, in what may be an even finer example of fiduciary prose—connoisseurs will\n",
|
||
"wish to read it backward as well as forward—the Board warned the public: When\n",
|
||
"[the Board] finds that conditions are arising which obstruct the Federal reserve\n",
|
||
"banks in the effective discharge of their functions of so managing the credit\n",
|
||
"facilities of the Federal reserve system as to accommodate commerce and\n",
|
||
"business, it is its duty to inquire into them and to take such measures as may\n",
|
||
"be deemed suitable and effective in the circumstances to correct them; which, in\n",
|
||
"the immediate situation, means to restrain the use, either directly or\n",
|
||
"indirectly, of Federal Reserve facilities in aid of the growth of speculative\n",
|
||
"credit.9 Almost simultaneously with this warning came the news that the Bank of\n",
|
||
"England was raising the bank rate from 4)4 to 5)4 per cent in an effort to\n",
|
||
"diminish the flow of British funds to the new Golconda. The result was a sharp\n",
|
||
"break in the market. On February 7, in a five-million share day, the Times\n",
|
||
"industrials dropped 11 points, with a further drop on the day following.\n",
|
||
"Thereafter the market recovered, but, for February as a whole, there was no\n",
|
||
"appreciable net gain. Economists have long had a phrase for this action—it was\n",
|
||
"called the Federal Reserve's effort at \"moral suasion.\" Since the market was\n",
|
||
"only temporarily checked, there has been ever since virtually complete agreement\n",
|
||
"that moral suasion was a failure.Precisely the opposite conclusion could and\n",
|
||
"probably should have been drawn. It is impossible to imagine a milder, more\n",
|
||
"tentative, more palpably panic-stricken communiqué than that issued by the\n",
|
||
"Board. The statement that the Board had no intention of interfering with loans\n",
|
||
"to support speculation so long as Federal Reserve credit was not involved is\n",
|
||
"especially noteworthy. Clearly the Federal Reserve was less interested in\n",
|
||
"checking speculation than in detaching itself from responsibility for the\n",
|
||
"speculation that was going on. And it will be observed that some anonymous\n",
|
||
"draftsman achieved a wording which indicated that not the present level but only\n",
|
||
"a further growth in speculation would be viewed with alarm. Yet in the then\n",
|
||
"state of nervousness even these almost incredibly feeble words caused a sharp\n",
|
||
"setback. VThe nervousness of the market and the unsuspected moral\n",
|
||
"authority of the equally nervous men of the Federal Reserve was even better\n",
|
||
"illustrated in March. As the new month approached, Mr. Coolidge made his blithe\n",
|
||
"observation about stocks being cheap and the country being sound. The market\n",
|
||
"surged up in what the papers dubbed \"The Inaugural Market,\" and on March 4, his\n",
|
||
"attitude toward speculators still unknown, Mr. Hoover took over. The market for\n",
|
||
"the next couple of weeks remained strong.Then toward the end of the month\n",
|
||
"disquieting news reached Wall Street. The Federal Reserve Board was meeting\n",
|
||
"daily in Washington. It issued no statements. Newspapermen pressed the members\n",
|
||
"after the sessions and were met with what then, as now, was known as tight-\n",
|
||
"lipped silence. There was not a hint as to what the meetings were about,\n",
|
||
"although everyone knew they concerned the market. The meetings continued day\n",
|
||
"after day, and there was also an unprecedented Saturday session.Soon it was too\n",
|
||
"much. On Monday, March 25, the first market day following the unseemly Saturday\n",
|
||
"meeting, the tension became unbearable. Although, or rather because, Washington\n",
|
||
"was still silent, people began to sell. Speculative favorites—Commercial\n",
|
||
"Solvents, Wright Aero, American Railway Express—dropped 10 or 12 points or more;\n",
|
||
"the Times industrial average was off 9½ points for the day. More important, some\n",
|
||
"banks decided that, in the event of a Federal Reserve crackdown, virtue might\n",
|
||
"have a reward above revenue. They began curtailing their loans in the call\n",
|
||
"market, and the rate on brokers' loans went to 14 per cent. On the next day,\n",
|
||
"Tuesday, March 26, everything was much worse. The Federal Reserve Board was\n",
|
||
"still maintaining its by now demoralizing silence. A wave of fear swept the\n",
|
||
"market. More people decided to sell, and they sold in astonishing volume. An\n",
|
||
"amazing 8,246,740 shares changed hands on the New York Stock Exchange, far above\n",
|
||
"any previous record. Prices seemed to drop vertically. At the low for the day\n",
|
||
"20– and 30-point losses were commonplace. The Times industrials at one time were\n",
|
||
"15 points below the previous day's close. Thousands of speculators, in whose\n",
|
||
"previous experience the market had always risen, now saw for the first time the\n",
|
||
"seamy side of their new way of life. Each new quotation was far below the last\n",
|
||
"one. Moreover, the ticker, unable to cope with the unprecedented volume, was far\n",
|
||
"behind the market. Not only were things bad, but they had almost certainly\n",
|
||
"become worse, yet one couldn't tell from the ticker how much worse. Before the\n",
|
||
"day was out many of these thousands received a peremptory telegram from their\n",
|
||
"brokers—a telegram that was in the starkest of contrast with the encouraging,\n",
|
||
"half-confidential, rich-uncle tone of all previous communications. It asked more\n",
|
||
"margin promptly.Meanwhile the banks continued to trim sails for the storm. It is\n",
|
||
"probable that some professional traders were selling because they foresaw the\n",
|
||
"moment when there would be no money with which to carry stocks on margin. And\n",
|
||
"that moment might be near, for on the morning of March 26, the rate on call\n",
|
||
"money reached 20 per cent, its high for the 1929 boom. March 26, 1929, could\n",
|
||
"have been the end. Money could have remained tight. The authorities might have\n",
|
||
"remained firm in their intention to keep it so. The panic might have continued.\n",
|
||
"Each fall in prices would have forced a new echelon of speculators to sell, and\n",
|
||
"so forced prices down still more. It did not happen, and if any man can be\n",
|
||
"credited with this, the credit belongs to Charles E. Mitchell. The Federal\n",
|
||
"Reserve authorities were ambivalent, but Mitchell was not. He was for the boom.\n",
|
||
"Moreover, his prestige as head of one of the two largest and most influential\n",
|
||
"commercial banks, his reputation as an aggressive and highly successful\n",
|
||
"investment banker, and his position as a director of the New York Federal\n",
|
||
"Reserve Bank meant that he spoke with at least as much authority as anyone in\n",
|
||
"Washington. During the day, as money tightened, rates rose, and the market fell,\n",
|
||
"Mitchell decided to take a hand. He told the press, \"We feel that we have an\n",
|
||
"obligation which is paramount to any Federal Reserve warning, or anything else,\n",
|
||
"to avert any dangerous crisis in the money market.\" The National City, he said,\n",
|
||
"would loan money as necessary to prevent liquidation. It would also (and did)\n",
|
||
"borrow from the New York Federal Reserve Bank to do what the Federal Reserve\n",
|
||
"Board had warned against doing. Disguised only slightly by the prose form of\n",
|
||
"finance, Mitchell issued the Wall Street counterpart of Mayor Hague's famous\n",
|
||
"manifesto, \"I am the law in Jersey City.\"Mitchell's words were like magic. By\n",
|
||
"the end of trading on the 26th money rates had eased, and the market had\n",
|
||
"rallied. The Federal Reserve remained silent, but now its silence was\n",
|
||
"reassuring. It meant that it conceded Mitchell's mastery. The next day the\n",
|
||
"National City regularized its commitment to the boom: it announced that it would\n",
|
||
"insure reasonable interest rates by putting $25 million into the call market—$5\n",
|
||
"million when the rate was 16 per cent, and $5 million additional for each\n",
|
||
"percentage point. In its monthly letter a few days later, the bank justified its\n",
|
||
"position and, incidentally, gave an admirable statement of the dilemma which the\n",
|
||
"Federal Reserve faced. (The National City was in no doubt which horn of the\n",
|
||
"dilemma it preferred.) The letter said: \"The National City Bank fully recognizes\n",
|
||
"the dangers of overspeculation [sic] and endorses the desire of the Federal\n",
|
||
"Reserve authorities to restrain excessive credit expansion for this purpose. At\n",
|
||
"the same time, the bank, business generally, and it may be assumed the Federal\n",
|
||
"Reserve Banks ... wish to avoid a general collapse of the securities markets\n",
|
||
"such as would have a disastrous effect on business.\"10 VIMitchell did\n",
|
||
"not escape criticism. A Senate investigation was mentioned. Senator Carter\n",
|
||
"Class, who as the sponsor of the Federal Reserve legislation had a keen\n",
|
||
"proprietary interest in its operation, said: \"He avows his superior obligation\n",
|
||
"to a frantic stock market over against the obligation of his oath as a director\n",
|
||
"of the New York Federal Reserve Bank ... the Bank should ask for [his] immediate\n",
|
||
"resignation.\" That the Federal Reserve Board never dreamed of doing any such\n",
|
||
"thing may be taken as a further indication that its moral suasion was being\n",
|
||
"administered with a very infirm touch. And the Federal Reserve was criticized\n",
|
||
"even more than Mitchell—even though it could hardly have done less than it did.\n",
|
||
"Arthur Brisbane said judiciously: \"If buying and selling stocks is wrong the\n",
|
||
"government should close the Stock Exchange. If not, the Federal Reserve should\n",
|
||
"mind its own business.\" In a leading article in Barron's, a Mr. Seth Axley was\n",
|
||
"less even-handed: \"For the Federal Reserve Board to deny investors the means of\n",
|
||
"recognizing economies which are now proved, skill which is now learned, and\n",
|
||
"inventions which are almost unbelievable seems to justify doubt whether it is\n",
|
||
"adequately interpreting the times.\"11 Since the principal action which the\n",
|
||
"Federal Reserve had taken against investors had been to hold meetings and\n",
|
||
"maintain silence, this was doubtless a trifle harsh. However, it was positively\n",
|
||
"kind as compared with the words of a young Princeton scholar who emerged at this\n",
|
||
"time as a leading defender of Wall Street.Joseph Stagg Lawrence's book Wall\n",
|
||
"Street and Washington, when it appeared later in the year bearing the\n",
|
||
"distinguished imprimatur of the Princeton University Press, was described by a\n",
|
||
"leading financial journal as a \"breath of fresh air.\" The regulatory concern of\n",
|
||
"the Federal Reserve about Wall Street, Mr. Lawrence said in this remarkable\n",
|
||
"volume, was motivated strictly by bias—a bias \"founded upon a clash of interests\n",
|
||
"and a moral and intellectual antipathy between the wealthy, cultured, and\n",
|
||
"conservative settlements on the seacoast [including Wall Street] and the poverty\n",
|
||
"stricken, illiterate, and radical pioneer communities of the interior.\" 12 The\n",
|
||
"cultured and conservative Mr. Lawrence also had hard words for the defenders of\n",
|
||
"the Federal Reserve in the Senate, including, oddly enough, the seacoast senator\n",
|
||
"from Virginia, Carter Glass. \"It seems incredible that in the year of our Grace\n",
|
||
"1929 a body of presumably intelligent public men should permit fanatical\n",
|
||
"passions and provincial ignorance to find expression in unrestrained virulence.\n",
|
||
"Yet this is precisely what has taken place ... When the Senator from the Old\n",
|
||
"Dominion rose in that chamber of absurdities, sometimes referred to as a\n",
|
||
"deliberative assembly, his remarks were characterized by neither reason nor\n",
|
||
"restraint. Blatant bigotry and turbulent provincialism have joined to condemn an\n",
|
||
"innocent community.\"13 Some hardened Wall Streeters may have been surprised when\n",
|
||
"they realized that the \"innocent community\" meant them. VIIAfter the\n",
|
||
"defeat by Mitchell in March, the Federal Reserve retired from the field. There\n",
|
||
"continued to be some slight anxiety as to what it might do. In April, William\n",
|
||
"Crapo Durant is supposed to have paid a secret night visit to the White House to\n",
|
||
"wam President Hoover that if the Board were not called off it would precipitate\n",
|
||
"a terrible crash. The President was noncommittal, and Durant is said to have\n",
|
||
"reduced his holdings before leaving on a trip to Europe.14 In June from\n",
|
||
"Princeton Mr. Lawrence said that the Board was still \"doing its utmost to cast\n",
|
||
"the proverbial monkey wrench into the machinery of prosperity.\" He warned the\n",
|
||
"Board that it had \"aroused the enmity of an honest, intelligent, and public-\n",
|
||
"spirited community.\"15 (This also was Wall Street.) But the Board, in fact, had\n",
|
||
"decided to leave that honest, intelligent, and public-spirited community\n",
|
||
"strictly to its own devices. It realized, Governor Young said subsequently, that\n",
|
||
"\"while the hysteria might be somewhat restrained,\" it would have to run its\n",
|
||
"course, and the Reserve Banks could only brace themselves for the \"inevitable\n",
|
||
"collapse.\"16 More accurately, the Federal Reserve authorities had decided not to\n",
|
||
"be responsible for the collapse. In August the Board finally agreed to an\n",
|
||
"increase in the rediscount rate to 6 per cent. The market weakened only for a\n",
|
||
"day. Any conceivable consequence of the action was nullified by a simultaneous\n",
|
||
"easing of the buying rate on acceptances.In fact, from the end of March on, the\n",
|
||
"market had nothing further to fear from authority. President Hoover did ask\n",
|
||
"Henry M. Robinson, a Los Angeles banker, to proceed as his emissary to New York\n",
|
||
"and talk to the bankers there about the boom. According to Mr. Hoover, Robinson\n",
|
||
"was assured that things were sound.17 Richard Whitney, the Vice-President of the\n",
|
||
"Exchange, was also summoned to the White House and told that something should be\n",
|
||
"done about speculation. Nothing was done, and Mr. Hoover was able to find some\n",
|
||
"solace in the thought that primary responsibility for regulating the stock\n",
|
||
"exchange rested with the Governor of New York, Franklin D. Roosevelt.18\n",
|
||
"Roosevelt, too, was following a laissez-faire policy, at least on the matter of\n",
|
||
"the stock market. A firm of investment counselors in Boston, called McNeel's\n",
|
||
"Financial Service, describing itself with a deft Back Bay touch as \"An\n",
|
||
"Aristocracy of Successful Investors,\" advertised a new guide to investment. The\n",
|
||
"headline read: \"He made $70,000 after reading Beating the Stock Market\" No doubt\n",
|
||
"whoever it was did. He might have made it without reading the volume or without\n",
|
||
"being able to read. For now, free at last from all threat of government reaction\n",
|
||
"or retribution, the market sailed off into the wild blue yonder. Especially\n",
|
||
"after June 1 all hesitation disappeared. Never before or since have so many\n",
|
||
"become so wondrously, so effortlessly, and so quickly rich. Perhaps Messrs.\n",
|
||
"Hoover and Mellon, and the Federal Reserve were right in keeping their hands\n",
|
||
"off. Perhaps it was worth being poor for a long time to be so rich for just a\n",
|
||
"little while. CHAPTER IIIIn Goldman, Sachs We TrustTHE RECONDITE\n",
|
||
"PROBLEMS of Federal Reserve policy were not the only questions that were\n",
|
||
"agitating Wall Street intellectuals in the early months of 1929. There was worry\n",
|
||
"that the country might be running out of common stocks. One reason prices of\n",
|
||
"stocks were so high, it was explained, was that there weren't enough to go\n",
|
||
"around, and, accordingly, they had acquired a \"scarcity value.\" Some issues, it\n",
|
||
"was said, were becoming so desirable that they would soon be taken out of the\n",
|
||
"market and would not reappear at any price.If, indeed, common stocks were\n",
|
||
"becoming scarce it was in spite of as extraordinary a response of supply to\n",
|
||
"demand as any in the history of that well-worn relationship. Without doubt, the\n",
|
||
"most striking feature of the financial era which ended in the autumn of 1929 was\n",
|
||
"the desire of people to buy securities and the effect of this on values. But the\n",
|
||
"increase in the number of securities to buy was hardly less striking. And the\n",
|
||
"ingenuity and zeal with which companies were devised in which securities might\n",
|
||
"be sold was as remarkable as anything.Not all of the increase in the volume of\n",
|
||
"securities in 1928 and 1929 was for the sole purpose of accommodating the\n",
|
||
"speculator. It was a good time to raise money for general corporate purposes.\n",
|
||
"Investors would supply capital with enthusiasm and without tedious questions.\n",
|
||
"(Seaboard Air Line was a speculative favorite of the period in part because many\n",
|
||
"supposed it to be an aviation stock with growth possibilities.) In these years\n",
|
||
"of prosperity men with a vision of still greater prosperity stretching on and on\n",
|
||
"and forever, naturally saw the importance of being well provided with plant and\n",
|
||
"working capital. This was no time to be niggardly. Also, it was an age of\n",
|
||
"consolidation, and each new merger required, inevitably, some new capital and a\n",
|
||
"new issue of securities to pay for it. A word must be said about the merger\n",
|
||
"movement of the twenties.It was not the first such movement but, in many\n",
|
||
"respects, it was the first of its kind. Just before and just after the turn of\n",
|
||
"the century in industry after industry, small companies were combined into large\n",
|
||
"ones. The United States Steel Corporation, International Harvester,\n",
|
||
"International Nickel, American Tobacco, and numerous other of the great\n",
|
||
"corporations trace to this period. In these cases the firms which were combined\n",
|
||
"produced the same or related products for the same national market. The primary\n",
|
||
"motivation in all but the rarest cases was to reduce, eliminate, or regularize\n",
|
||
"competition. Each of the new giants dominated an industry, and henceforth\n",
|
||
"exercised measurable control over prices and production, and perhaps also over\n",
|
||
"investment and the rate of technological innovation.A few such mergers occurred\n",
|
||
"in the twenties. Mostly, however, the mergers of this period brought together\n",
|
||
"not firms in competition with each other but firms doing the same thing in\n",
|
||
"different communities. Local electric, gas, water, bus, and milk companies were\n",
|
||
"united in great regional or national systems. The purpose was not to eliminate\n",
|
||
"competition, but rather the incompetence, somnambulance, naïveté, or even the\n",
|
||
"unwarranted integrity of local managements. In the twenties, a man in downtown\n",
|
||
"New York or Chicago could take unabashed pride in the fact that he was a\n",
|
||
"financial genius. The local owners and managers were not. There was no false\n",
|
||
"modesty when it came to citing the advantages of displacing yokels with a\n",
|
||
"central management of decent sophistication. In the case of utilities the\n",
|
||
"instrument for accomplishing this centralization of management and control was\n",
|
||
"the holding company. These bought control of the operating companies. On\n",
|
||
"occasion they bought control of other holding companies which controlled yet\n",
|
||
"other holding companies, which in turn, directly or indirectly through yet other\n",
|
||
"holding companies, controlled the operating companies. Everywhere local power,\n",
|
||
"gas, and water companies passed into the possession of a holding-company\n",
|
||
"system.Food retailing, variety stores, department stores, and motion picture\n",
|
||
"theatres showed a similar, although not precisely identical, development. Here,\n",
|
||
"too, local ownership gave way to central direction and control. The instrument\n",
|
||
"of this centralization, however, was not the holding company but the corporate\n",
|
||
"chain. These, more often than not, instead of taking over existing businesses,\n",
|
||
"established new outlets.The holding companies issued securities in order to buy\n",
|
||
"operating properties, and the chains issued securities in order to build new\n",
|
||
"stores and theatres. While in the years before 1929 the burgeoning utility\n",
|
||
"systems—Associated Gas and Electric, Commonwealth and Southern, and the Insull\n",
|
||
"companies—attracted great attention, the chains were at least as symbolic of the\n",
|
||
"era. Montgomery Ward was one of the prime speculative favorites of the period;\n",
|
||
"it owed its eminence to the fact that it was a chain and thus had a particularly\n",
|
||
"bright future. The same was true of Woolworth, American Stores, and others.\n",
|
||
"Interest in branch and chain banking was also strong, and it was widely felt\n",
|
||
"that state and federal laws were an archaic barrier to a consolidation which\n",
|
||
"would knit the small-town and small-city banks into a few regional and national\n",
|
||
"systems. Various arrangements for defeating the intent of the law, most notably\n",
|
||
"bank holding companies, were highly regarded.Inevitably promoters organized some\n",
|
||
"new companies merely to capitalize on the public interest in industries with a\n",
|
||
"new and wide horizon and provide securities to sell. Radio and aviation stocks\n",
|
||
"were believed to have a particularly satisfactory prospect, and companies were\n",
|
||
"formed which never had more than a prospect. In September 1929, an advertisement\n",
|
||
"in the Times called attention to the impending arrival of television and said\n",
|
||
"with considerable prescience that the \"commercial possibilities of this new art\n",
|
||
"defy imagination.\" The ad opined, somewhat less presciently, that sets would be\n",
|
||
"in use in homes that fall. However, in the main, the market boom of 1929 was\n",
|
||
"rooted directly or indirectly in existing industries and enterprises. New and\n",
|
||
"fanciful issues for new and fanciful purposes, ordinarily so important in times\n",
|
||
"of speculation, played a relatively small part. No significant amount of stock\n",
|
||
"was sold in companies \"To make Salt Water Fresh—For building of Hospitals for\n",
|
||
"Bastard Children—For building of Ships against Pirates—For importing a Number of\n",
|
||
"large Jack Asses from Spain,\" or even \"For a Wheel of Perpetual Motion,\" to cite\n",
|
||
"a representative list of promotions at the time of the South Sea Bubble.1\n",
|
||
"IIThe most notable piece of speculative architecture of the late twenties, and\n",
|
||
"the one by which, more than any other device, the public demand for common\n",
|
||
"stocks was satisfied, was the investment trust or company. The investment trust\n",
|
||
"did not promote new enterprises or enlarge old ones. It merely arranged that\n",
|
||
"people could own stock in old companies through the medium of new ones. Even in\n",
|
||
"the United States, in the twenties, there were limits to the amount of real\n",
|
||
"capital which existing enterprises could use or new ones could be created to\n",
|
||
"employ. The virtue of the investment trust was that it brought about an almost\n",
|
||
"complete divorce of the volume of corporate securities outstanding from the\n",
|
||
"volume of corporate assets in existence. The former could be twice, thrice, or\n",
|
||
"any multiple of the latter. The volume of underwriting business and of\n",
|
||
"securities available for trading on the exchanges all expanded accordingly. So\n",
|
||
"did the securities to own, for the investment trusts sold more securities than\n",
|
||
"they bought. The difference went into the call market, real estate, or the\n",
|
||
"pockets of the promoters. It is hard to imagine an invention better suited to\n",
|
||
"the time or one better designed to eliminate the anxiety about the possible\n",
|
||
"shortage of common stocks. The idea of the investment trust is an old one,\n",
|
||
"although, oddly enough, it came late to the United States. Since the eighteen-\n",
|
||
"eighties in England and Scotland, investors, mostly smaller ones, had pooled\n",
|
||
"their resources by buying stock in an investment company. The latter, in turn,\n",
|
||
"invested the funds so secured. A typical trust held securities in from five\n",
|
||
"hundred to a thousand operating companies. As a result, the man with a few\n",
|
||
"pounds, or even a few hundred, was able to spread his risk far more widely than\n",
|
||
"were he himself to invest. And the management of the trusts could be expected to\n",
|
||
"have a far better knowledge of companies and prospects in Singapore, Madras,\n",
|
||
"Capetown, and the Argentine, places to which British funds regularly found their\n",
|
||
"way, than the widow in Bristol or the doctor in Glasgow. The smaller risk and\n",
|
||
"better information well justified the modest compensation of those who managed\n",
|
||
"the enterprise. Despite some early misadventures, the investment trusts soon\n",
|
||
"became an established part of the British scene. Before 1921 in the United\n",
|
||
"States only a few small companies existed for the primary purpose of investing\n",
|
||
"in the securities of other companies.2 In that year, interest in investment\n",
|
||
"trusts began to develop, partly as the result of a number of newspaper and\n",
|
||
"magazine articles describing the English and Scottish trusts. The United States,\n",
|
||
"it was pointed out, had not been keeping abreast of the times; other countries\n",
|
||
"were excelling us in fiduciary innovation. Soon, however, we began to catch up.\n",
|
||
"More trusts were organized, and by the beginning of 1927 an estimated 160 were\n",
|
||
"in existence. Another 140 were formed during that year.3The managers of the\n",
|
||
"British trusts normally enjoy the greatest of discretion in investing the funds\n",
|
||
"placed at their disposal. At first the American promoters were wary of asking\n",
|
||
"for such a vote of confidence. Many of the early trusts were trusts—the investor\n",
|
||
"bought an interest in a specified assortment of securities which were then\n",
|
||
"deposited with a trust company. At the least the promoters committed themselves\n",
|
||
"to a rigorous set of rules on the kinds of securities to be purchased and the\n",
|
||
"way they were to be held and managed. But as the twenties wore along, such\n",
|
||
"niceties disappeared. The investment trust became, in fact, an investment\n",
|
||
"corporation.4 It sold its securities to the public—sometimes just common stock,\n",
|
||
"more often common and preferred stock, debenture and mortgage bonds—and the\n",
|
||
"proceeds were then invested as the management saw fit. Any possible tendency of\n",
|
||
"the common stockholder to interfere with the management was prevented by selling\n",
|
||
"him non-voting stock or having him assign his voting rights to a management-\n",
|
||
"controlled voting trust.For a long time the New York Stock Exchange looked with\n",
|
||
"suspicion on the investment trusts; only in 1929 was fisting permitted. Even\n",
|
||
"then the Committee on the Stock List required an investment trust to post with\n",
|
||
"the Exchange the book and market value of the securities held at the time of\n",
|
||
"listing and once a year thereafter to provide an inventory of its holdings. This\n",
|
||
"provision confined the listing of most of the investment trusts to the Curb,\n",
|
||
"Boston, Chicago, or other road company exchanges. Apart from its convenience,\n",
|
||
"this refusal to disclose holdings was thought to be a sensible precaution.\n",
|
||
"Confidence in the investment judgment of the managers of the trusts was very\n",
|
||
"high. To reveal the stocks they were selecting might, it was said, set off a\n",
|
||
"dangerous boom in the securities they favored. Historians have told with wonder\n",
|
||
"of one of the promotions at the time of the South Sea Bubble. It was \"For an\n",
|
||
"Undertaking which shall in due time be revealed.\" The stock is said to have sold\n",
|
||
"exceedingly well. As promotions the investment trusts were, on the record, more\n",
|
||
"wonderful. They were undertakings the nature of which was never to be revealed,\n",
|
||
"and their stock also sold exceedingly well. IIIDuring 1928 an estimated\n",
|
||
"186 investment trusts were organized; by the early months of 1929 they were\n",
|
||
"being promoted at the rate of approximately one each business day, and a total\n",
|
||
"of 265 made their appearance during the course of the year. In 1927 the trusts\n",
|
||
"sold to the public about $400,000,000 worth of securities; in 1929 they marketed\n",
|
||
"an estimated three billions worth. This was at least a third of all the new\n",
|
||
"capital issues in that year; by the autumn of 1929 the total assets of the\n",
|
||
"investment trusts were estimated to exceed eight billions of dollars. They had\n",
|
||
"increased approximately elevenfold since the beginning of 1927.5 The\n",
|
||
"parthenogenesis of an investment trust differed from that of an ordinary\n",
|
||
"corporation. In nearly all cases it was sponsored by another company, and by\n",
|
||
"1929 a surprising number of different kinds of concerns were bringing the trusts\n",
|
||
"into being. Investment banking houses, commercial banks, brokerage firms,\n",
|
||
"securities dealers, and, most important, other investment trusts were busy\n",
|
||
"giving birth to new trusts. The sponsors ranged in dignity from the House of\n",
|
||
"Morgan, sponsor of the United and Alleghany Corporations, down to one Chauncey\n",
|
||
"D. Parker, the head of a fiscally perilous investment banking firm in Boston,\n",
|
||
"who organized three investment trusts in 1929 and sold $25,000,000 worth of\n",
|
||
"securities to an eager public. Chauncey then lost most of the proceeds and\n",
|
||
"lapsed into bankruptcy.6Sponsorship of a trust was not without its rewards. The\n",
|
||
"sponsoring firm normally executed a management contract with its offspring.\n",
|
||
"Under the usual terms, the sponsor ran the investment trust, invested its funds,\n",
|
||
"and received a fee based on a percentage of capital or earnings. Were the\n",
|
||
"sponsor a stock exchange firm, it also received commissions on the purchase and\n",
|
||
"sale of securities for its trust. Many of the sponsors were investment banking\n",
|
||
"firms, which meant, in effect, that the firm was manufacturing securities it\n",
|
||
"could then bring to market. This was an excellent way of insuring an adequate\n",
|
||
"supply of business. The enthusiasm with which the public sought to buy\n",
|
||
"investment trust securities brought the greatest rewards of all. Almost\n",
|
||
"invariably people were willing to pay a sizable premium over the offering price.\n",
|
||
"The sponsoring firm (or its promoters) received allotments of stock or warrants\n",
|
||
"which entitled them to stock at the offering price. These they were then able to\n",
|
||
"sell at once at a profit. Thus one of the enterprises of the Mr. Chauncey D.\n",
|
||
"Parker just mentioned—a company with the resounding name of Seaboard Utilities\n",
|
||
"Shares Corporation—issued 1,600,000 shares of common stock on which the company\n",
|
||
"netted $10.32 a share. That, however, was not the price paid by the public. It\n",
|
||
"was the price at which the stock was issued to Parker and his colleagues. They\n",
|
||
"in turn sold their shares to the public at from $11 to $18.25 and split the\n",
|
||
"profit with the dealers who marketed the securities.7Operations of this sort\n",
|
||
"were not confined to the lowly or the vaguely disreputable. J. P. Morgan and\n",
|
||
"Company, which (with Bonbright and Company) sponsored United Corporation in\n",
|
||
"January 1929, offered a package of one share of common stock and one of\n",
|
||
"preferred to a list of friends, Morgan partners included, at $75. This was a\n",
|
||
"bargain. When trading in United Corporation began a week later, the price was 92\n",
|
||
"bid, 94 asked on the over-the-counter market, and after four days the stock\n",
|
||
"reached 99. Stock that had been taken up at 75 could be and was promptly resold\n",
|
||
"at these prices.8 That such agreeable incentives greatly stimulated the\n",
|
||
"organization of new investment trusts is hardly surprising. IVThere were\n",
|
||
"some, indeed, who only regretted that everyone could not participate in the\n",
|
||
"gains from these new engines of financial progress. One of those who had\n",
|
||
"benefited from the United Corporation promotion just mentioned was John J.\n",
|
||
"Raskob. As Chairman of the Democratic National Committee, he was also\n",
|
||
"politically committed to a firm friendship for the people. He believed that\n",
|
||
"everyone should be in on the kind of opportunities he himself enjoyed.One of the\n",
|
||
"fruits of this generous impulse during the year was an article in the Ladies'\n",
|
||
"Home Journal with the attractive title, \"Everybody Ought to be Rich.\" In it Mr.\n",
|
||
"Raskob pointed out that anyone who saved fifteen dollars a month, invested it in\n",
|
||
"sound common stocks, and spent no dividends would be worth—as it then\n",
|
||
"appeared—some eighty thousand dollars after twenty years. Obviously, at this\n",
|
||
"rate, a great many people could be rich.But there was the twenty-year delay.\n",
|
||
"Twenty years seemed a long time to get rich, especially in 1929, and for a\n",
|
||
"Democrat and friend of the people to commit himself to such gradualism was to\n",
|
||
"risk being thought a reactionary. Mr. Raskob, therefore, had a further\n",
|
||
"suggestion. He proposed an investment trust which would be specifically designed\n",
|
||
"to allow the poor man to increase his capital just as the rich man was doing.The\n",
|
||
"plan, which Mr. Raskob released to the public in the early summer of 1929, was\n",
|
||
"worked out in some detail. (The author stated that he had discussed it with\n",
|
||
"\"financiers, economists, theorists, professors, bankers, labor leaders,\n",
|
||
"industrial leaders, and many men of no prominence who have ideas.\") A company\n",
|
||
"would be organized to buy stocks. The proletarian with, say, $200 would turn\n",
|
||
"over his pittance to the company which would then buy stocks in the rather less\n",
|
||
"meager amount of $500. The additional $300 the company would get from a\n",
|
||
"financial subsidiary organized for the purpose, and with which it would post all\n",
|
||
"of the stock as collateral. The incipient capitalist would pay off his debt at\n",
|
||
"the rate of perhaps $25 a month. He would, of course, get the full benefit of\n",
|
||
"the increase in the value of the stock, and this was something that Mr. Raskob\n",
|
||
"regarded as inevitable. Hammering home the inadequacy of existing arrangements,\n",
|
||
"Mr. Raskob said: \"Now all the man with $200 to $500 to invest can do today is to\n",
|
||
"buy Liberty bonds...\"9The reaction to the Raskob plan was comparable to the\n",
|
||
"response to a new and daring formulation of the relation of mass to energy. \"A\n",
|
||
"practical Utopia,\" one paper called it. Another described it as \"The greatest\n",
|
||
"vision of Wall Street's greatest mind.\" A tired and cynical commentator was\n",
|
||
"moved to say that it looks \"more like financial statesmanship than anything that\n",
|
||
"has come out of Wall Street in many a weary moon.\"10Had there been a little more\n",
|
||
"time, it seems certain that something would have been made of Mr. Raskob's plan.\n",
|
||
"People were full of enthusiasm for the wisdom and perspicacity of such men. This\n",
|
||
"was admirably indicated by the willingness of people to pay for the genius of\n",
|
||
"the professional financier. VThe measure of this respect for financial\n",
|
||
"genius was the relation of the market value of the outstanding securities of the\n",
|
||
"investment trusts to the value of the securities it owned. Normally the\n",
|
||
"securities of the trust were worth considerably more than the property it owned.\n",
|
||
"Sometimes they were worth twice as much. There should be no ambiguity on this\n",
|
||
"point. The only property of the investment trust was the common and preferred\n",
|
||
"stocks and debentures, mortgages, bonds, and cash that it owned. (Often it had\n",
|
||
"neither office nor office furniture; the sponsoring firm ran the investment\n",
|
||
"trust out of its own quarters.) Yet, had these securities all been sold on the\n",
|
||
"market, the proceeds would invariably have been less, and often much less, than\n",
|
||
"the current value of the outstanding securities of the investment company. The\n",
|
||
"latter, obviously, had some claim to value which went well beyond the assets\n",
|
||
"behind them. That premium was, in effect, the value an admiring community placed\n",
|
||
"on professional financial knowledge, skill, and manipulative ability. To value a\n",
|
||
"portfolio of stocks \"at the market\" was to regard it only as inert property. But\n",
|
||
"as the property of an investment trust it was much more, for the portfolio was\n",
|
||
"then combined with the precious ingredient of financial genius. Such special\n",
|
||
"ability could invoke a whole strategy for increasing the value of securities. It\n",
|
||
"could join in pools and syndicates to put up values. It knew when others were\n",
|
||
"doing likewise and could go along. Above all, the financial genius was in on\n",
|
||
"things. It had access to what Mr. Lawrence of Princeton described as \"the stage\n",
|
||
"whereon is focused the world's most intelligent and best informed judgment of\n",
|
||
"the values of the enterprises which serve men's needs.\"11 One might make money\n",
|
||
"investing directly in Radio, J. I. Case, or Montgomery Ward, but how much safer\n",
|
||
"and wiser to let it be accomplished by the men of peculiar knowledge, and\n",
|
||
"wisdom.By 1929 the investment trusts were aware of their reputation for\n",
|
||
"omniscience, as well as its importance, and they lost no opportunity to enlarge\n",
|
||
"it. To have a private economist was one possibility, and as the months passed a\n",
|
||
"considerable competition developed for those men of adequate reputation and\n",
|
||
"susceptibility. It was a golden age for professors. The American Founders Group,\n",
|
||
"an awe-inspiring family of investment trusts, had as a director Professor Edwin\n",
|
||
"W. Kemmerer, the famous Princeton money expert. The staff economist was Dr.\n",
|
||
"Rufus Tucker, also a well-known figure. (That economists were not yet\n",
|
||
"functioning with perfect foresight is perhaps suggested by the subsequent\n",
|
||
"history of the enterprise. United Founders, the largest company in the group,\n",
|
||
"suffered a net contraction in its assets of $301,385,504 by the end of 1935, and\n",
|
||
"its stock dropped from a high of over $75 share in 1929 to a little under 75\n",
|
||
"cents.)12 Still another great combine was advised by Dr. David Friday, who had\n",
|
||
"come to Wall Street from the University of Michigan. Friday's reputation for\n",
|
||
"both insight and foresight was breathtaking. A Michigan trust had three college\n",
|
||
"professors—Irving Fisher of Yale, Joseph S. Davis of Stanford, and Edmund E. Day\n",
|
||
"then of Michigan—to advise on its policies.13 The company stressed not only the\n",
|
||
"diversity of its portfolio but also of its counsel. It was fully protected from\n",
|
||
"any parochial Yale, Stanford, or Michigan view of the market.Other trusts urged\n",
|
||
"the excellence of their genius in other terms. Thus one observed that, since it\n",
|
||
"owned stocks in 120 corporations, it benefited from the \"combined efficiency of\n",
|
||
"their presidents, officers, and the boards of directors.\" It noted further that\n",
|
||
"\"closely allied to these corporations are the great banking institutions.\" Then,\n",
|
||
"in something of a logical broad jump, it concluded, \"The trust, therefore,\n",
|
||
"mobilizes to a large extent the successful business intellect of the country.\"\n",
|
||
"Another concern, less skilled in logical method, contented itself with pointing\n",
|
||
"out that \"Investing is a science instead of a 'one-man job.'\"14As 1929 wore\n",
|
||
"along, it was plain that more and more of the new investors in the market were\n",
|
||
"relying on the intellect and the science of the trusts. This meant, of course,\n",
|
||
"that they still had the formidable problem of deciding between the good and the\n",
|
||
"bad trusts. That there were some bad ones was (though barely) recognized.\n",
|
||
"Writing in the March 1929 issue of The Atlantic Monthly, Paul C. Cabot stated\n",
|
||
"that dishonesty, inattention, inability, and greed were among the common\n",
|
||
"shortcomings of the new industry. These were impressive disadvantages, and as an\n",
|
||
"organizer and officer of a promising investment trust, the State Street\n",
|
||
"Investment Corporation, Mr. Cabot presumably spoke with some authority.15\n",
|
||
"However, audience response to such warnings in 1929 was very poor. And the\n",
|
||
"warnings were very infrequent. VIKnowledge, manipulative skill, or\n",
|
||
"financial genius were not the only magic of the investment trust. There was also\n",
|
||
"leverage. By the summer of 1929, one no longer spoke of investment trusts as\n",
|
||
"such. One referred to high-leverage trusts, low-leverage trusts, or trusts\n",
|
||
"without any leverage at all.The principle of leverage is the same for an\n",
|
||
"investment trust as in the game of crack-the-whip. By the application of well-\n",
|
||
"known physical laws, a modest movement near the point of origin is translated\n",
|
||
"into a major jolt on the extreme periphery. In an investment trust leverage was\n",
|
||
"achieved by issuing bonds, preferred stock, as well as common stock to purchase,\n",
|
||
"more or less exclusively, a portfolio of common stocks. When the common stock so\n",
|
||
"purchased rose in value, a tendency which was always assumed, the value of the\n",
|
||
"bonds and preferred stock of the trust was largely unaffected.16 These\n",
|
||
"securities had a fixed value derived from a specified return. Most or all of the\n",
|
||
"gain from rising portfolio values was concentrated on the common stock of the\n",
|
||
"investment trust which, as a result, rose marvelously. Consider, by way of\n",
|
||
"illustration, the case of an investment trust organized in early 1929 with a\n",
|
||
"capital of $150 million—a plausible size by then. Let it be assumed, further,\n",
|
||
"that a third of the capital was realized from the sale of bonds, a third from\n",
|
||
"preferred stock, and the rest from the sale of common stock. If this $150\n",
|
||
"million were invested, and if the securities so purchased showed a normal\n",
|
||
"appreciation, the portfolio value would have increased by midsummer by about 50\n",
|
||
"per cent. The assets would be worth $225 million. The bonds and preferred stock\n",
|
||
"would still be worth only $100 million; their earnings would not have increased,\n",
|
||
"and they could claim no greater share of the assets in the hypothetical event of\n",
|
||
"a liquidation of the company. The remaining $125 million, therefore, would\n",
|
||
"underlie the value of the common stock of the trust. The latter, in other words,\n",
|
||
"would have increased in asset value from $50 million to $125 million, or by 150\n",
|
||
"per cent, and as the result of an increase of only 50 per cent in the value of\n",
|
||
"the assets of the trust as a whole.This was the magic of leverage, but this was\n",
|
||
"not all of it. Were the common stock of the trust, which had so miraculously\n",
|
||
"increased in value, held by still another trust with similar leverage, the\n",
|
||
"common stock of that trust would get an increase of between 700 and 800 per cent\n",
|
||
"from the original 50 per cent advance. And so forth. In 1929 the discovery of\n",
|
||
"the wonders of the geometric series struck Wall Street with a force comparable\n",
|
||
"to the invention of the wheel. There was a rush to sponsor investment trusts\n",
|
||
"which would sponsor investment trusts, which would, in turn, sponsor investment\n",
|
||
"trusts. The miracle of leverage, moreover, made this a relatively costless\n",
|
||
"operation to the ultimate man behind all of the trusts. Having launched one\n",
|
||
"trust and retained a share of the common stock, the capital gains from leverage\n",
|
||
"made it relatively easy to swing a second and larger one which enhanced the\n",
|
||
"gains and made possible a third and still bigger trust. Thus, Harrison Williams,\n",
|
||
"one of the more ardent exponents of leverage, was thought by the Securities and\n",
|
||
"Exchange Commission to have substantial influence over a combined investment\n",
|
||
"trust and holding company system with a market value in 1929 at close to a\n",
|
||
"billion dollars.17 This had been built on his original control of a smallish\n",
|
||
"concern—the Central States Electric Corporation—which was worth only some six\n",
|
||
"million dollars in 1921.18 Leverage was also a prime factor in the remarkable\n",
|
||
"growth of the American Founders Group. The original member of this notable\n",
|
||
"family of investment trusts was launched in 1921. The original promoter was,\n",
|
||
"unhappily, unable to get the enterprise off the ground because he was in\n",
|
||
"bankruptcy. However, the following year a friend contributed $500, with which\n",
|
||
"modest capital a second trust was launched, and the two companies began\n",
|
||
"business. The public reception was highly favorable, and by 1927 the two\n",
|
||
"original companies and a third which had subsequently been added had sold\n",
|
||
"between seventy and eighty million dollars worth of securities to the public.19\n",
|
||
"But this was only the beginning; in 1928 and 1929 an explosion of activity\n",
|
||
"struck the Founders Group. Stock was sold to the public at a furious rate. New\n",
|
||
"firms with new names were organized to sell still more stock until, by the end\n",
|
||
"of 1929, there were thirteen companies in the group. At that time the largest\n",
|
||
"company, the United Founders Corporation, had total resources of $686,165,000.\n",
|
||
"The group as a whole had resources with a market value of more than a billion\n",
|
||
"dollars, which may well have been the largest volume of assets ever controlled\n",
|
||
"by an original outlay of $500. Of the billion dollars, some $320,000,000 was\n",
|
||
"represented by inter-company holdings—the investment of one or another company\n",
|
||
"of the group in the securities of yet others. This fiscal incest was the\n",
|
||
"instrument through which control was maintained and leverage enjoyed. Thanks to\n",
|
||
"this long chain of holdings by one company in another, the increases in values\n",
|
||
"in 1928 and 1929 were effectively concentrated in the value of the common stock\n",
|
||
"of the original companies.Leverage, it was later to develop, works both ways.\n",
|
||
"Not all of the securities held by the Founders were of a kind calculated to rise\n",
|
||
"indefinitely, much less to resist depression. Some years later the portfolio was\n",
|
||
"found to have contained 5000 shares of Kreuger and Toll, 20,000 shares of Kolo\n",
|
||
"Products Corporation, an adventuresome new company which was to make soap out of\n",
|
||
"banana oil, and $295,000 in the bonds of the Kingdom of Yugoslavia.20 As Kreuger\n",
|
||
"and Toll moved down to its ultimate value of nothing, leverage was also at\n",
|
||
"work—geometric series are equally dramatic in reverse. But this aspect of the\n",
|
||
"mathematics of leverage was still unrevealed in early 1929, and notice must\n",
|
||
"first be taken of the most dramatic of all the investment company promotions of\n",
|
||
"that remarkable year, those of Goldman, Sachs. VIIGoldman, Sachs and\n",
|
||
"Company, an investment banking and brokerage partnership, came rather late to\n",
|
||
"the investment trust business. Not until December 4, 1928, less than a year\n",
|
||
"before the stock market crash, did it sponsor the Goldman Sachs Trading\n",
|
||
"Corporation, its initial venture in the field. However, rarely, if ever, in\n",
|
||
"history has an enterprise grown as the Goldman Sachs Trading Corporation and its\n",
|
||
"offspring grew in the months ahead.The initial issue of stock in the Trading\n",
|
||
"Corporation was a million shares, all of which was bought by Goldman, Sachs and\n",
|
||
"Company at $100 a share for a total of $100,000,000. Ninety per cent was then\n",
|
||
"sold to the public at $104. There were no bonds and no preferred stocks;\n",
|
||
"leverage had not yet been discovered by Goldman, Sachs and Company. Control of\n",
|
||
"the Goldman Sachs Trading Corporation remained with Goldman, Sachs and Company\n",
|
||
"by virtue of a management contract and the presence of the partners of the\n",
|
||
"company on the board of the Trading Corporation.21In the two months after its\n",
|
||
"formation, the new company sold some more stock to the public, and on February\n",
|
||
"21 it merged with another investment trust, the Financial and Industrial\n",
|
||
"Securities Corporation. The assets of the resulting company were valued at $235\n",
|
||
"million, reflecting a gain of well over 100 per cent in under three months. By\n",
|
||
"February 2, roughly three weeks before the merger, the stock for which the\n",
|
||
"original investors had paid $104 was selling for $136.50. Five days later, on\n",
|
||
"February 7, it reached $222.50. At this latter figure it had a value\n",
|
||
"approximately twice that of the current total worth of the securities, cash, and\n",
|
||
"other assets owned by the Trading Corporation. This remarkable premium was not\n",
|
||
"the undiluted result of public enthusiasm for the financial genius of Goldman,\n",
|
||
"Sachs. Goldman, Sachs had considerable enthusiasm for itself, and the Trading\n",
|
||
"Corporation was buying heavily of its own securities. By March 14 it had bought\n",
|
||
"560,724 shares of its own stock for a total outlay of $57,021,936.22 This, in\n",
|
||
"turn, had boomed their value. However, perhaps foreseeing the exiguous character\n",
|
||
"of an investment company which had its investments all in its own common stock,\n",
|
||
"the Trading Corporation stopped buying itself in March. Then it resold part of\n",
|
||
"the stock to William Crapo Durant, who re-resold it to the public as opportunity\n",
|
||
"allowed.The spring and early summer were relatively quiet for Goldman, Sachs,\n",
|
||
"but it was a period of preparation. By July 26 it was ready. On that date the\n",
|
||
"Trading Corporation, jointly with Harrison Williams, launched the Shenandoah\n",
|
||
"Corporation, the first of two remarkable trusts. The initial securities issue by\n",
|
||
"Shenandoah was $102,500,000 (there was an additional issue a couple of months\n",
|
||
"later) and it was reported to have been oversubscribed some sevenfold. There\n",
|
||
"were both preferred and common stock, for by now Goldman, Sachs knew the\n",
|
||
"advantages of leverage. Of the five million shares of common stock in the\n",
|
||
"initial offering, two million were taken by the Trading Corporation, and two\n",
|
||
"million by Central States Electric Corporation on behalf of the cosponsor,\n",
|
||
"Harrison Williams. Williams was a member of the small board along with partners\n",
|
||
"in Goldman, Sachs. Another board member was a prominent New York attorney whose\n",
|
||
"lack of discrimination in this instance may perhaps be attributed to youthful\n",
|
||
"optimism. It was Mr. John Foster Dulles. The stock of Shenandoah was issued at\n",
|
||
"$17.50. There was brisk trading on a \"when issued\" basis. It opened at 30,\n",
|
||
"reached a high of 36, and closed at 36, or 18.5 above the issue price. (By the\n",
|
||
"end of the year the price was 8 and a fraction. It later touched fifty cents.)\n",
|
||
"Meanwhile Goldman, Sachs was already preparing its second tribute to the\n",
|
||
"countryside of Thomas Jefferson, the prophet of small and simple enterprises.\n",
|
||
"This was the even mightier Blue Ridge Corporation, which made its appearance on\n",
|
||
"August 20. Blue Ridge had a capital of $142,000,000, and nothing about it was\n",
|
||
"more remarkable than the fact that it was sponsored by Shenandoah, its precursor\n",
|
||
"by precisely twenty-five days. Blue Ridge had the same board of directors as\n",
|
||
"Shenandoah, including the still optimistic Mr. Dulles, and of its 7,250,000\n",
|
||
"shares of common stock (there was also a substantial issue of preferred)\n",
|
||
"Shenandoah subscribed a total of 6,250,000. Goldman, Sachs by now was applying\n",
|
||
"leverage with a vengeance.An interesting feature of Blue Ridge was the\n",
|
||
"opportunity it offered the investor to divest himself of routine securities in\n",
|
||
"direct exchange for the preferred and common stock of the new corporation. A\n",
|
||
"holder of American Telephone and Telegraph Company could receive 4 Wis shares\n",
|
||
"each of Blue Ridge Preference and Common for each share of Telephone stock\n",
|
||
"turned in. The same privilege was extended to holders of Allied Chemical and\n",
|
||
"Dye, Santa Fe, Eastman Kodak, General Electric, Standard Oil of New Jersey, and\n",
|
||
"some fifteen other stocks. There was much interest in this offer. August 20, the\n",
|
||
"birthday of Blue Ridge, was a Tuesday, but there was more work to be done by\n",
|
||
"Goldman, Sachs that week. On Thursday, the Goldman Sachs Trading Corporation\n",
|
||
"announced the acquisition of the Pacific American Associates, a West Coast\n",
|
||
"investment trust which, in turn, had recently bought a number of smaller\n",
|
||
"investment trusts and which owned the American Trust Company, a large commercial\n",
|
||
"bank with numerous branches throughout California. Pacific American had a\n",
|
||
"capital of around a hundred million. In preparation for the merger, the Trading\n",
|
||
"Corporation had issued another $71,400,000 in stock which it had exchanged for\n",
|
||
"capital stock of the American Company, the holding company which owned over 99\n",
|
||
"per cent of the common stock of the American Trust Company.23Having issued more\n",
|
||
"than a quarter of a billion dollars worth of securities in less than a month—an\n",
|
||
"operation that would not then have been unimpressive for the United States\n",
|
||
"Treasury—activity at Goldman, Sachs subsided somewhat. Its members had not been\n",
|
||
"the only busy people during this time. It was a poor day in August and September\n",
|
||
"of that year when no new trust was announced or no large new issue of securities\n",
|
||
"was offered by an old one. Thus, on August first, the papers announced the\n",
|
||
"formation of Anglo-American Shares, Inc., a company which, with a soigné touch\n",
|
||
"not often seen in a Delaware corporation, had among its directors the Marquess\n",
|
||
"of Carisbrooke, GCB, GCVO, and Colonel, the Master of Sempill, AFC, otherwise\n",
|
||
"identified as the President of the Royal Aeronautical Society, London. American\n",
|
||
"Insuranstocks Corporation was launched the same day, though boasting no more\n",
|
||
"glamorous a director than William Cibbs McAdoo. On succeeding days came Gude\n",
|
||
"Winmill Trading Corporation, National Republic Investment Trust, Insull Utility\n",
|
||
"Investments, Inc., International Carriers, Ltd., Tri-Continental Allied\n",
|
||
"Corporation, and Solvay American Investment Corporation. On August 13 the papers\n",
|
||
"also announced that an Assistant U.S. Attorney had visited the offices of the\n",
|
||
"Cosmopolitan Fiscal Corporation and also an investment service called the\n",
|
||
"Financial Counselor. In both cases the principals were absent. The offices of\n",
|
||
"the Financial Counselor were equipped with a peephole like a speakeasy. More\n",
|
||
"investment trust securities were offered in September of 1929 even than in\n",
|
||
"August—the total was above $600 million.24 However, the nearly simultaneous\n",
|
||
"promotion of Shenandoah and Blue Ridge was to stand as the pinnacle of new era\n",
|
||
"finance. It is difficult not to marvel at the imagination which was implicit in\n",
|
||
"this gargantuan insanity. If there must be madness something may be said for\n",
|
||
"having it on a heroic scale.Years later, on a gray dawn in Washington, the\n",
|
||
"following colloquy occurred before a committee of the United States\n",
|
||
"Senate.25 Senator Couzens. Did Goldman, Sachs and Company organize the Goldman\n",
|
||
"Sachs Trading Corporation?Mr. Sachs. Yes, sir. Senator Couzens. And it sold its\n",
|
||
"stock to the public?Mr. Sachs. A portion of it. The firm invested originally in\n",
|
||
"10 per cent of the entire issue for the sum of $10,000,000.Senator Couzens. And\n",
|
||
"the other 90 per cent was sold to the public?Mr. Sachs. Yes, sir.Senator\n",
|
||
"Couzens. At what price?Mr. Sachs. At 104. That is the old stock ... the stock\n",
|
||
"was split two for one.Senator Couzens. And what is the price of the stock\n",
|
||
"now?Mr. Sachs. Approximately 1⅓. CHAPTER IVThe Twilight of IllusionTHERE\n",
|
||
"WAS no summer lull in Wall Street that year. Along with the great investment\n",
|
||
"trust promotions went the greatest market ever. Every day prices rose; they\n",
|
||
"almost never fell. During June the Times industrials went up 52 points; in July\n",
|
||
"they gained another 25. This was a total gain of 77 points in two months. In all\n",
|
||
"of the remarkable year of 1928 they had gone up only 86.5 points. Then in August\n",
|
||
"they rose another 33 points. This gain of 110 points in three months—from 339 on\n",
|
||
"the last day of May to 449 on the last day of August—meant that during the\n",
|
||
"summer values had increased, over-all, by nearly a quarter.Individual issues had\n",
|
||
"also done very well. During the three summer months, Westinghouse went from 151\n",
|
||
"to 286 for a net gain of 135. General Electric was up from 268 to 391, and Steel\n",
|
||
"from 165 to 258. Even so somber a security as American Tel and Tel had gone from\n",
|
||
"209 to 303. The investment trusts were making good gains. United Founders went\n",
|
||
"from 36 bid to 68; Alleghany Corporation rose from 33 to 56.The volume of\n",
|
||
"trading was also consistently heavy. On the New York Stock Exchange it was\n",
|
||
"frequently between four and five million shares. Only occasionally on a full day\n",
|
||
"did it drop below three million. However, trading on the New York Stock Exchange\n",
|
||
"was no longer a good index of the total interest in securities speculation. Many\n",
|
||
"new and exciting issues—Shenandoah, Blue Ridge, Pennroad, Insull Utilities—were\n",
|
||
"not listed on the Big Board. The New York Stock Exchange in those days was not a\n",
|
||
"snobbish, prying, or an intolerant institution. Most companies that so desired\n",
|
||
"could have their stocks listed. Nevertheless, there were some who found it wise,\n",
|
||
"and many more who found it convenient not to answer the fairly elementary\n",
|
||
"requests which were made by the Exchange for information. The new stocks,\n",
|
||
"accordingly, were traded on the Curb, or in Boston, or on other out-of-town\n",
|
||
"exchanges. Although business on the New York Stock Exchange remained larger than\n",
|
||
"that on all other markets combined, its relative position suffered. (In 1929 it\n",
|
||
"had an estimated 61 per cent of all transactions; three years later, when most\n",
|
||
"of the new trusts had disappeared forever, the Exchange had 76 per cent of the\n",
|
||
"total business.)1 It follows that by the summer of 1929 the normally\n",
|
||
"somnambulant markets of Boston, San Francisco, and even of Cincinnati were\n",
|
||
"having a boom. Instead of being merely a pale reflection of the real thing on\n",
|
||
"Wall Street, they had a life and personality of their own. Stocks were for sale\n",
|
||
"here that couldn't be had in New York, and some of them had an exceptional\n",
|
||
"speculative lack. By 1929 it was a poor town, sadly devoid of civic spirit,\n",
|
||
"which wasn't wondering if it too shouldn't have a stock market. More than the\n",
|
||
"prices of common stocks were rising. So, at an appalling rate, was the volume of\n",
|
||
"speculation. Brokers' loans during the summer increased at a rate of about\n",
|
||
"$400,000,000 a month. By the end of the summer, the total exceeded seven\n",
|
||
"billions. Of that more than half was being supplied by corporations and\n",
|
||
"individuals, at home and abroad, who were taking advantage of the excellent rate\n",
|
||
"of return which New York was providing on money. Only rarely did the rate on\n",
|
||
"call loans during that summer get as low as six per cent. The normal range was\n",
|
||
"seven to twelve. On one occasion the rate touched fifteen. Since, as earlier\n",
|
||
"observed, these loans provided all but total safety, liquidity, and ease of\n",
|
||
"administration, the interest would not have seemed unattractive to a usurious\n",
|
||
"moneylender in Bombay. To a few alarmed observers it seemed as though Wall\n",
|
||
"Street were by way of devouring all the money of the entire world. However, in\n",
|
||
"accordance with the cultural practice, as the summer passed, the sound and\n",
|
||
"responsible spokesmen decried not the increase in brokers' loans, but those who\n",
|
||
"insisted on attaching significance to this trend. There was a sharp criticism of\n",
|
||
"the prophets of doom. IIThere were two sources of intelligence on\n",
|
||
"brokers' loans. One was the monthly tabulation of the New York Stock Exchange,\n",
|
||
"which in general is used here. The other was the slightly less complete return\n",
|
||
"of the Federal Reserve System which was published weekly. Each Friday this\n",
|
||
"report showed a large increase in loans; each Friday it was firmly stated that\n",
|
||
"it didn't mean a thing, and anyone who suggested otherwise was administered a\n",
|
||
"stern rebuke. It seems probable that only a minority of the people in the market\n",
|
||
"related the volume of the brokers' loans to the volume of purchases on margin\n",
|
||
"and thence to the amount of speculation. Accordingly, an expression of concern\n",
|
||
"over these loans was easily attacked as a gratuitous effort to undermine\n",
|
||
"confidence. Thus, in Barron's on July 8, Sheldon Sinclair Wells explained that\n",
|
||
"those who worried about brokers' loans, and about the influx of funds from\n",
|
||
"corporations, simply did not know what was going on. The call market had become\n",
|
||
"a great new investment outlet for corporate reserves, he argued. The critics did\n",
|
||
"not appreciate this change. Chairman Mitchell of the National City Bank, by\n",
|
||
"nature an equable man, was repeatedly angered by the attention being given to\n",
|
||
"brokers' loans and expressed himself strongly. The financial press was also\n",
|
||
"disturbed, and when Arthur Brisbane later in the year questioned the propriety\n",
|
||
"of a 10 per cent call rate, The Wall Street Journal reached the end of its\n",
|
||
"patience. \"Even in general newspapers some accurate knowledge is required for\n",
|
||
"discussing most things. Why is it that any ignoramus can talk about Wall\n",
|
||
"Street?\"2 (It does seem possible that Brisbane thought the rate was 10 per cent\n",
|
||
"a day rather than a year.)Scholars also reacted against those who, deliberately\n",
|
||
"or otherwise, were sabotaging prosperity with their unguarded pessimism. After\n",
|
||
"soberly viewing the situation, Professor Dice concluded that the high level of\n",
|
||
"brokers' loans should not be \"as greatly feared as some would have us believe.\"\n",
|
||
"3 In August the Midland Bank of Cleveland made public the results of\n",
|
||
"calculations which proved that until loans by corporations in the stock market\n",
|
||
"reached twelve billion there was no cause for concern.4The best reassurance on\n",
|
||
"brokers' loans was in the outlook for the market. If stocks remained high and\n",
|
||
"went higher, and if they did so because their prospects justified their price,\n",
|
||
"then there was no occasion to worry about the loans that were piling up.\n",
|
||
"Accordingly, much of the defense of the loans consisted in defending the levels\n",
|
||
"of the market. It was not hard to persuade people that the market was sound; as\n",
|
||
"always in such times they asked only that the disturbing voices of doubt be\n",
|
||
"muted and that there be tolerably frequent expressions of confidence. In 1929\n",
|
||
"treason had not yet become a casual term of reproach. As a result, pessimism was\n",
|
||
"not openly equated with efforts to destroy the American way of life. Yet it had\n",
|
||
"such connotations. Almost without exception, those who expressed concern said\n",
|
||
"subsequently that they did so with fear and trepidation. (Later in the year a\n",
|
||
"Boston firm of investment counselors struck a modern note with a widely\n",
|
||
"publicized warning that America had no place for \"destructionists.\")The official\n",
|
||
"optimists were many and articulate. Thus in June, Bernard Baruch told Bruce\n",
|
||
"Barton, in a famous interview published in The American Magazine5 that \"the\n",
|
||
"economic condition of the world seems on the verge of a great forward movement.\"\n",
|
||
"He pointed out that no bears had houses on Fifth Avenue. Numerous college\n",
|
||
"professors also exuded scientific confidence. In light of later developments,\n",
|
||
"the record of the Ivy League was especially unfortunate. In a statement which\n",
|
||
"achieved minor notoriety, Lawrence of Princeton said that \"the consensus of\n",
|
||
"judgment of the millions whose valuations function on that admirable market, the\n",
|
||
"Stock Exchange, is that stocks are not at present over-valued.\" He added: \"Where\n",
|
||
"is that group of men with the all-embracing wisdom which will entitle them to\n",
|
||
"veto the judgment of this intelligent multitude?\"6That autumn Professor Irving\n",
|
||
"Fisher of Yale made his immortal estimate: \"Stock prices have reached what looks\n",
|
||
"like a permanently high plateau.\" Irving Fisher was the most original of\n",
|
||
"American economists. Happily there are better things —his contributions to index\n",
|
||
"numbers, technical economic theory, and monetary theory—for which he is\n",
|
||
"remembered.From Cambridge slightly less spacious reassurance came from the\n",
|
||
"Harvard Economic Society, an extracurricular enterprise conducted by a number of\n",
|
||
"economics professors of unexceptionable conservatism. The purpose of the Society\n",
|
||
"was to help businessmen and speculators foretell the future. Forecasts were made\n",
|
||
"several times a month and undoubtedly gained in stature from their association\n",
|
||
"with the august name of the university.By wisdom or good luck, the Society in\n",
|
||
"early 1929 was mildly bearish. Its forecasters had happened to decide that a\n",
|
||
"recession (though assuredly not a depression) was overdue. Week by week they\n",
|
||
"foretold a slight setback in business. When, by the summer of 1929, the setback\n",
|
||
"had not appeared, at least in any very visible form, the Society gave up and\n",
|
||
"confessed error. Business, it decided, might be good after all. This, as such\n",
|
||
"things are judged, was still a creditable record, but then came the crash. The\n",
|
||
"Society remained persuaded that no serious depression was in prospect. In\n",
|
||
"November it said firmly that \"a severe depression like that of 1920–21 is\n",
|
||
"outside the range of probability. We are not facing protracted liquidation.\"\n",
|
||
"This view the Society reiterated until it was liquidated. IIIThe bankers\n",
|
||
"were also a source of encouragement to those who wished to believe in the\n",
|
||
"permanence of the boom. A great many of them abandoned their historic role as\n",
|
||
"the guardians of the nation's fiscal pessimism and enjoyed a brief respite of\n",
|
||
"optimism. They had reasons for doing so. In the years preceding, a considerable\n",
|
||
"number of the commercial banks, including the largest of the New York houses,\n",
|
||
"had organized securities affiliates. These affiliates sold stocks and bonds to\n",
|
||
"the public, and this business had become important. It was a business that\n",
|
||
"compelled a rosy view of the future. In addition, individual bankers, perhaps\n",
|
||
"taking a cue from the heads of the National City and Chase in New York, were\n",
|
||
"speculating vigorously on their own behalf. They were unlikely to say, much less\n",
|
||
"to advocate, anything that would jar the market. However, there were exceptions.\n",
|
||
"One was Paul M. Warburg of the International Acceptance Bank, whose predictions\n",
|
||
"must be accorded the same prominence as the forecasts of Irving Fisher. They\n",
|
||
"were remarkably prescient. In March of 1929, he called for a stronger Federal\n",
|
||
"Reserve policy and argued that if the present orgy of \"unrestrained speculation\"\n",
|
||
"were not brought promptly to a halt there would ultimately be a disastrous\n",
|
||
"collapse. This, he suggested, would be unfortunate not alone for the\n",
|
||
"speculators. It would \"bring about a general depression involving the entire\n",
|
||
"country.\"7Only Wall Street spokesmen who took the most charitable view of\n",
|
||
"Warburg contented themselves with describing him as obsolete. One said he was\n",
|
||
"\"sandbagging American prosperity.\" Others hinted that he had a motive—presumably\n",
|
||
"a short position. As the market went up and up, his warnings were recalled only\n",
|
||
"with contempt.8The most notable skeptics were provided by the press. They were a\n",
|
||
"great minority to be sure. Most magazines and most newspapers in 1929 reported\n",
|
||
"the upward sweep of the market with admiration and awe and without alarm. They\n",
|
||
"viewed both the present and the future with exuberance. Moreover, by 1929\n",
|
||
"numerous journalists were sternly resisting the more subtle blandishments and\n",
|
||
"flattery to which they have been thought susceptible. Instead they were\n",
|
||
"demanding cold cash for news favorable to the market. A financial columnist of\n",
|
||
"the Daily News, who signed himself \"The Trader,\" received some $19,000 in 1929\n",
|
||
"and early 1930 from a free-lance operator named John J. Levenson. \"The Trader\"\n",
|
||
"repeatedly spoke well of stocks in which Mr. Levenson was interested. Mr.\n",
|
||
"Levenson later insisted, however, that this was a coincidence and that the\n",
|
||
"payment reflected his more or less habitual generosity.9 A radio commentator\n",
|
||
"named William J. McMahon was the president of the McMahon Institute of Economic\n",
|
||
"Research, an organization that was mostly McMahon. He told in his broadcasts of\n",
|
||
"the brilliant prospects of stocks which pool operators were seeking to boom. For\n",
|
||
"this, it later developed, he received an honorarium of $250 a week from a\n",
|
||
"certain David M. Lion.10 Mr. Lion was one of several whom the Pecora Committee\n",
|
||
"reported as making a business of buying favorable comment in the necessary\n",
|
||
"amount at the proper moment. At the other extreme was the best of the financial\n",
|
||
"press. The established financial services like Poor's and that of the Standard\n",
|
||
"Statistics Company never lost touch with reality. In the autumn Poor's Weekly\n",
|
||
"Business and Investment Letter went so far as to speak of the \"great common-\n",
|
||
"stock delusion.\" 11 The editor of The Commercial and Financial Chronicle was\n",
|
||
"never quite shaken in his conviction that Wall Street had taken leave of its\n",
|
||
"senses. The weekly reports on the brokers' loans were regularly the occasion for\n",
|
||
"a solemn warning; the news columns featured any available bad news. However, by\n",
|
||
"far the greatest force for sobriety was the New York Times. Under the guidance\n",
|
||
"of the veteran Alexander Dana Noyes, its financial page was all but immune to\n",
|
||
"the blandishments of the New Era. A regular reader could not doubt that a day of\n",
|
||
"reckoning was expected. Also, on several occasions it reported, much too\n",
|
||
"prematurely, that the day of reckoning had arrived.Indeed the temporary breaks\n",
|
||
"in the market which preceded the crash were a serious trial for those who had\n",
|
||
"declined fantasy. Early in 1928, in June, in December, and in February and March\n",
|
||
"of 1929 it seemed that the end had come. On various of these occasions the Times\n",
|
||
"happily reported the return to reality. And then the market took flight again.\n",
|
||
"Only a durable sense of doom could survive such discouragement. The time was\n",
|
||
"coming when the optimists would reap a rich harvest of discredit. But it has\n",
|
||
"long since been forgotten that for many months those who resisted reassurance\n",
|
||
"were similarly, if less permanently, discredited. To say that the Times, when\n",
|
||
"the real crash came, reported the event with jubilation would be an\n",
|
||
"exaggeration. Nevertheless, it covered it with an unmistakable absence of\n",
|
||
"sorrow. IVBy the summer of 1929 the market not only dominated the news.\n",
|
||
"It also dominated the culture. That recherché minority which at other times has\n",
|
||
"acknowledged its interest in Saint Thomas Aquinas, Proust, psychoanalysis, and\n",
|
||
"psychosomatic medicine then spoke of United Corporation, United Founders, and\n",
|
||
"Steel. Only the most aggressive of the eccentrics maintained their detachment\n",
|
||
"from the market and their interest in autosuggestion or communism. Main Street\n",
|
||
"had always had one citizen who could speak knowingly about buying or selling\n",
|
||
"stocks. Now he became an oracle. In New York, on the edge of any gathering of\n",
|
||
"significantly interesting people there had long been a literate broker or\n",
|
||
"investment counselor who was abreast of current plans for pools, syndicates, and\n",
|
||
"mergers, and was aware of attractive possibilities. He helpfully advised his\n",
|
||
"friends on investments, and pressed, he would always tell what he knew of the\n",
|
||
"market and much that he didn't. Now these men, even in the company of artists,\n",
|
||
"playwrights, poets, and beautiful concubines, suddenly shone forth. Their words,\n",
|
||
"more or less literally, became golden. Their audience listened not with the\n",
|
||
"casual heed of people who are collecting quotable epigrams, but with the truly\n",
|
||
"rapt attention of those who expect to make money by what they hear. That much of\n",
|
||
"what was repeated about the market—then as now—bore no relation to reality is\n",
|
||
"important, but not remarkable. Between human beings there is a type of\n",
|
||
"intercourse which proceeds not from knowledge, or even from lack of knowledge,\n",
|
||
"but from failure to know what isn't known. This was true of much of the\n",
|
||
"discourse on the market. At luncheon in downtown Scranton, the knowledgeable\n",
|
||
"physician spoke of the impending split-up in the stock of Western Utility\n",
|
||
"Investors and the effect on prices. Neither the doctor nor his listeners knew\n",
|
||
"why there should be a split-up, why it should increase values, or even why\n",
|
||
"Western Utility Investors should have any value. But neither the doctor nor his\n",
|
||
"audience knew that he did not know. Wisdom, itself, is often an abstraction\n",
|
||
"associated not with fact or reality but with the man who asserts it and the\n",
|
||
"manner of its assertion.Perhaps the failure to visualize the extent of one's\n",
|
||
"innocence was especially true of women investors, who by now were entering the\n",
|
||
"market in increasing numbers. (An article in The North American Review in April\n",
|
||
"reported that women had become important players of \"man's most exciting\n",
|
||
"capitalistic game\" and that the modern housewife now \"reads, for instance, that\n",
|
||
"Wright Aero is going up ... just as she does that fresh fish is now on the\n",
|
||
"market...\" The author hazarded the guess that success in speculation would do a\n",
|
||
"lot for women's prestige.) To the typical female plunger the association of\n",
|
||
"Steel was not with a corporation, and certainly not with mines, ships,\n",
|
||
"railroads, blast furnaces, and open hearths. Rather it was with symbols on a\n",
|
||
"tape and lines on a chart and a price that went up. She spoke of Steel with the\n",
|
||
"familiarity of an old friend, when in fact she knew nothing of it whatever. Nor\n",
|
||
"would anyone tell her that she did not know that she did not know. We are a\n",
|
||
"polite and cautious people, and we avoid unpleasantness. Moreover, such advice,\n",
|
||
"so far from accomplishing any result, would only have inspired a feeling of\n",
|
||
"contempt for anyone who lacked the courage and the initiative and the\n",
|
||
"sophistication to see how easily one could become rich. The lady operator had\n",
|
||
"discovered she could be rich. Surely her right to be rich was as good as\n",
|
||
"anyone's. One of the uses of women is that their motivations, though often\n",
|
||
"similar, are less elaborately disguised than those of men.The values of a\n",
|
||
"society totally preoccupied with making money are not altogether reassuring.\n",
|
||
"During the summer, the Times accepted the copy of a dealer in the securities of\n",
|
||
"the National Waterworks Corporation, a company which had been organized to buy\n",
|
||
"into city water companies. The advertisement carried the following acquisitive\n",
|
||
"thought: \"Picture this scene today, if by some cataclysm only one small well\n",
|
||
"should remain for the great city of New York—$1.00 a bucket, $100, $1,000,\n",
|
||
"$1,000,000. The man who owned the well would own the wealth of the city.\" All\n",
|
||
"cataclysmically minded investors were urged to get a long position in water\n",
|
||
"before it was too late. VThe polar role of the stock market in American\n",
|
||
"life in the summer of 1929 is beyond doubt. And many people of many different\n",
|
||
"kinds and condition were in the stock market. Frederick Lewis Allen pictured the\n",
|
||
"diversity of this participation in a fine passage: The rich man's chauffeur\n",
|
||
"drove with his ears laid back to catch the news of an impending move in\n",
|
||
"Bethlehem Steel; he held fifty shares himself on a twenty-point margin. The\n",
|
||
"window-cleaner at the broker's office paused to watch the ticker, for he was\n",
|
||
"thinking of converting his laboriously accumulated savings into a few shares of\n",
|
||
"Simmons. Edwin Lefèvre (an articulate reporter on the market at this time who\n",
|
||
"could claim considerable personal experience ) told of a broker's valet who made\n",
|
||
"nearly a quarter of a million in the market, of a trained nurse who cleaned up\n",
|
||
"thirty thousand following the tips given her by grateful patients; and of a\n",
|
||
"Wyoming cattleman, thirty miles from the nearest railroad, who bought or sold a\n",
|
||
"thousand shares a day.12 Yet there is probably more danger of overestimating\n",
|
||
"rather than underestimating the popular interest in the market. The cliché that\n",
|
||
"by 1929 everyone \"was in the market\" is far from the literal truth. Then, as\n",
|
||
"now, to the great majority of workers, farmers, white-collar workers, indeed to\n",
|
||
"the great majority of all Americans, the stock market was a remote and vaguely\n",
|
||
"ominous thing. Then, as now, not many knew how one went about buying a security;\n",
|
||
"the purchase of stocks on margin was in every respect as remote from life as the\n",
|
||
"casino at Monte Carlo.In later years, a Senate committee investigating the\n",
|
||
"securities markets undertook to ascertain the number of people who were involved\n",
|
||
"in securities speculation in 1929. The member firms of twenty-nine exchanges in\n",
|
||
"that year reported themselves as having accounts with a total of 1,548,–707\n",
|
||
"customers. (Of these, 1,371,920 were customers of member firms of the New York\n",
|
||
"Stock Exchange.) Thus only one and a half million people, out of a population of\n",
|
||
"approximately 120 million and of between 29 and 30 million families, had an\n",
|
||
"active association of any sort with the stock market. And not all of these were\n",
|
||
"speculators. Brokerage firms estimated for the Senate committee that only about\n",
|
||
"600,000 of the accounts just mentioned were for margin trading, as compared with\n",
|
||
"roughly 950,000 in which trading was for cash. The figure of 600,000 for margin\n",
|
||
"traders involves some duplication—a few large operators had accounts with more\n",
|
||
"than one broker. There were also some traders whose operations were\n",
|
||
"insignificant. However, some speculators are included among the 950,000 cash\n",
|
||
"customers. Some were putting up the full purchase price for their securities,\n",
|
||
"although speculating nonetheless. Some were borrowing money outside the market\n",
|
||
"and posting the securities as collateral. Though listed as cash customers, they\n",
|
||
"were in effect buying on margin. However, it is safe to say that at the peak in\n",
|
||
"1929 the number of active speculators was less—and probably was much less—than a\n",
|
||
"million. Between the end of 1928 and the end of July of 1929, a period when the\n",
|
||
"popular folklore has Americans rushing like lemmings to participate in the\n",
|
||
"market, the number of margin accounts on all of the exchanges of the country\n",
|
||
"increased by only slightly more than fifty thousand.13 Hie striking thing about\n",
|
||
"the stock market speculation of 1929 was not the massiveness of the\n",
|
||
"participation. Rather it was the way it became central to the culture.\n",
|
||
"VIBy the end of the summer of 1929, brokers' bulletins and letters no longer\n",
|
||
"contented themselves with saying what stocks would rise that day and by how\n",
|
||
"much. They went on to say that at 2 P.M. Radio or General Motors would be \"taken\n",
|
||
"in hand.\"14 The conviction that the market had become the personal instrument of\n",
|
||
"mysterious but omnipotent men was never stronger. And, indeed, this was a period\n",
|
||
"of exceedingly active pool and syndicate operations—in short, of manipulation.\n",
|
||
"During 1929 more than a hundred issues on the New York Stock Exchange were\n",
|
||
"subject to manipulative operations, in which members of the Exchange or their\n",
|
||
"partners had participated. The nature of these operations varied somewhat but,\n",
|
||
"in a typical operation, a number of traders pooled their resources to boom a\n",
|
||
"particular stock. They appointed a pool manager, promised not to double-cross\n",
|
||
"each other by private operations, and the pool manager then took a position in\n",
|
||
"the stock which might also include shares contributed by the participants. This\n",
|
||
"buying would increase prices and attract the interest of people watching the\n",
|
||
"tape across the country. The interest of the latter would then be further\n",
|
||
"stimulated by active selling and buying, all of which gave the impression that\n",
|
||
"something big was afloat. Tipsheets and market commentators would tell of\n",
|
||
"exciting developments in the offing. If all went well, the public would come in\n",
|
||
"to buy, and prices would rise on their own. The pool manager would then sell\n",
|
||
"out, pay himself a percentage of the profits, and divide the rest with his\n",
|
||
"investors.15While it lasted, there was never a more agreeable way of making\n",
|
||
"money. The public at large sensed the attractiveness of these operations, and as\n",
|
||
"the summer passed it came to be supposed that Wall Street was concerned with\n",
|
||
"little else. This was an exaggeration, but it did not discourage public activity\n",
|
||
"in the market. People did not believe they were being shorn. Nor were they. Both\n",
|
||
"they and the pool operators were making money with the difference, only, that\n",
|
||
"the latter were making more. In any case, the public reaction to inside\n",
|
||
"operations was to hope that it might get some inside information on these\n",
|
||
"operations and so get a cut in the profits that the great men like Cutten,\n",
|
||
"Livermore, Raskob, and the rest were making. As the market came to be considered\n",
|
||
"less and less a long-run register of corporate prospects and more and more a\n",
|
||
"product of manipulative artifice, the speculator was required to give it his\n",
|
||
"closest, and preferably his undivided attention. Signs of incipient pool\n",
|
||
"activity had to be detected at the earliest possible moment, which meant that\n",
|
||
"one needed to have his eyes on the tape. However, even the person who was\n",
|
||
"relying on hunches, incantations, or simple faith, as distinct from the effort\n",
|
||
"to assess the intentions of the professionals, found it hard to be out of touch.\n",
|
||
"Only in the case of the rarest individuals can speculation be a part-time\n",
|
||
"activity. Money for most people is far too important. Of the South Sea Bubble it\n",
|
||
"was observed that \"Statesmen forgot their Politics, Lawyers the Bar, Merchants\n",
|
||
"their Traffic, Physicians their Patients, Tradesmen their Shops, Debtors of\n",
|
||
"Quality their Creditors, Divines the Pulpit, and even the Women themselves their\n",
|
||
"Pride and Vanity!\" 16 And so it was in 1929. \"Brokers' offices were crowded from\n",
|
||
"10 A.M. to 3 P.M. with seated or standing customers who, instead of attending to\n",
|
||
"their own business, were watching the blackboard. In some 'customers rooms' it\n",
|
||
"was difficult to get access to a spot from which the posted quotations could be\n",
|
||
"seen; no one could get a chance to inspect the tape.\" 17 It follows that to be\n",
|
||
"out of touch with the market, ever so briefly, was a nerve-wracking experience.\n",
|
||
"Happily, this was not often necessary. Ticker service was now nationwide; a\n",
|
||
"local telephone call would get the latest quotations from almost anywhere. A\n",
|
||
"journey to Europe provided one of the few troublesome exceptions. As The\n",
|
||
"Literary Digest pointed out during the course of the summer, \"Transoceanic\n",
|
||
"brokerage business has been growing to immense proportions ... But there has\n",
|
||
"been an interlude of uncertainty and inconvenience for speculators crossing the\n",
|
||
"ocean.\" 18 In August even this interlude was eliminated. Progressive brokerage\n",
|
||
"houses—a leader was M. J. Meehan, the specialist in Radio and a veteran of many\n",
|
||
"notable manipulations—installed branches on the big ships under special\n",
|
||
"regulations laid down by the Exchange. On August 17, the Leviathan and the lie\n",
|
||
"de France left port fully equipped for speculation on the high seas. Business on\n",
|
||
"the lie the opening day was described as brisk. One of the first transactions\n",
|
||
"was by Irving Berlin, who sold 1000 shares of Paramount-Famous-Lasky at 72. (It\n",
|
||
"was a shrewd move. The stock later went more or less to nothing and the company\n",
|
||
"into bankruptcy.)In Spokane an anonymous poet on the editorial staff of the\n",
|
||
"Spokesman-Review celebrated the seagoing boardrooms. We were crowded in the\n",
|
||
"cabin Watching figures on the Board; It was midnight on the ocean And a tempest\n",
|
||
"loudly roared. *** \"We are lost!\" the Captain shouted, As he staggered down the\n",
|
||
"stairs. I've got a tip,\" he faltered, \"Straight by wireless from the aunt Of a\n",
|
||
"fellow who's related To a cousin of Durant.\" At these awful words we shuddered,\n",
|
||
"And the stoutest bull grew sick While the brokers cried, \"More margin!\" And the\n",
|
||
"ticker ceased to tick. But the captain's little daughter Said, \"I do not\n",
|
||
"understand— Isn't Morgan on the ocean Just the same as on the land?\"19\n",
|
||
"VIILabor Day brought the summer of 1929 to its conventional end on September 2.\n",
|
||
"There was a severe heat wave, and on the evening of the holiday returning\n",
|
||
"motorists tied up the roads around New York for miles. In the end many were\n",
|
||
"forced to abandon their vehicles and make their way home by train or subway. On\n",
|
||
"September 3 the city continued to swelter in what the Weather Bureau reported\n",
|
||
"was the hottest day of the year.Away from Wall Street this was a very quiet day\n",
|
||
"in a very tranquil time. Years later Frederick Lewis Allen went to the\n",
|
||
"newspapers for that day and in a charming article told all that he found.20\n",
|
||
"There wasn't much. Disarmament was being discussed in that customarily desultory\n",
|
||
"fashion which doubtless in the end will destroy us. The Graf Zeppelin was\n",
|
||
"nearing the end of its first round-the-world flight. A tri-motored plane of\n",
|
||
"Transcontinental Air Transport had crashed in a thunderstorm in New Mexico, and\n",
|
||
"eight were killed. (The line had only recently inaugurated a forty-eight-hour\n",
|
||
"service to the West Coast—railway sleeper to Columbus, Ohio, plane to Waynoka,\n",
|
||
"Oklahoma, sleeper again to Clovis, New Mexico, and a plane the rest of the way.)\n",
|
||
"Babe Ruth had knocked out forty home runs so far in the season. As a best-\n",
|
||
"seller, All Quiet on the Western Front held a commanding lead over Dodsworth.\n",
|
||
"Women's dresses were all emphasizing a decidedly flat look without anyone's\n",
|
||
"saying so. From Washington it was announced that Harry F. Sinclair, then in the\n",
|
||
"District of Columbia jail for being in contempt of the Senate during the Teapot\n",
|
||
"Dome investigations, would henceforth be more closely confined. Previously he\n",
|
||
"had been going daily by automobile to the office of the jail physician whom he\n",
|
||
"was serving as a \"pharmaceutical assistant.\" Earlier in the year Sinclair's\n",
|
||
"stock market operations were on a huge scale, and they were later the subject of\n",
|
||
"detailed investigation. It was never shown whether the Washington sojourn\n",
|
||
"involved any important interruption. It seems most unlikely that it did. Mr.\n",
|
||
"Sinclair was one of the most resourceful and resilent entrepreneurs of his\n",
|
||
"generation. On September 3 sales on the New York Stock Exchange were 4,438,910\n",
|
||
"shares; call money was 9 per cent all day; the rate at the banks on prime\n",
|
||
"commercial paper was 6½ per cent; the rediscount rate at the New York Federal\n",
|
||
"Reserve Bank was 6 per cent. The market was strong with what the market\n",
|
||
"reporters called a good undertone.American Tel and Tel reached 304 that day.\n",
|
||
"U.S. Steel reached 262; General Electric was 396; J. I. Case, 350; New York\n",
|
||
"Central, 256; Radio Corporation of America, adjusted for earlier split-ups and\n",
|
||
"still not having paid a dividend, was 505. The brokers' loan figures of the\n",
|
||
"Federal Reserve, when they were released, also showed another huge\n",
|
||
"increase—$137,000,000 in one week. The New York banks were also borrowing\n",
|
||
"heavily from the Federal Reserve to carry the speculative superstructure—their\n",
|
||
"borrowings during the week increased by $64,000,000. In August the flow of gold\n",
|
||
"to New York from abroad had continued large. Yet the new month seemed to be\n",
|
||
"opening well. There were several expressions of confidence. On September 3, by\n",
|
||
"common consent, the great bull market of the nineteen-twenties came to an end.\n",
|
||
"Economics, as always, vouchsafes us few dramatic turning points. Its events are\n",
|
||
"invariably fuzzy or even indeterminate. On some days that followed—a few\n",
|
||
"only—some averages were actually higher. However, never again did the market\n",
|
||
"manifest its old confidence. The later peaks were not peaks but brief\n",
|
||
"interruptions of a downward trend.On September 4, the tone of the market was\n",
|
||
"still good, and then on September 5 came a break. The Times industrials dropped\n",
|
||
"10 points, and many individual stocks much more. The blue chips held up fairly\n",
|
||
"well, although Steel went from 255 to 246, while Westinghouse lost 7 points and\n",
|
||
"Tel and Tel 6. Volume mounted sharply as people sought to unload, and 5,565,280\n",
|
||
"shares were traded on the New York Stock Exchange.The immediate cause of the\n",
|
||
"break was clear—and interesting. Speaking before his Annual National Business\n",
|
||
"Conference on September 5, Roger Babson observed, \"Sooner or later a crash is\n",
|
||
"coming, and it may be terrific.\" He suggested that what had happened in Florida\n",
|
||
"would now happen in Wall Street, and with customary precision stated that the\n",
|
||
"(Dow-Jones) market averages would probably drop 60–80 points. In a burst of\n",
|
||
"cheer he concluded that \"factories will shut down ... men will be thrown out of\n",
|
||
"work ... the vicious circle will get in full swing and the result will be a\n",
|
||
"serious business depression.\"21 This was not exactly reassuring. Yet it was a\n",
|
||
"problem why the market suddenly should pay attention to Babson. As many hastened\n",
|
||
"to say, he had made many predictions before, and they had not affected prices\n",
|
||
"much one way or another. Moreover, Babson was not a man who inspired confidence\n",
|
||
"as a prophet in the manner of Irving Fisher or the Harvard Economic Society. As\n",
|
||
"an educator, philosopher, theologian, statistician, forecaster, economist, and\n",
|
||
"friend of the law of gravity, he had sometimes been thought to spread himself\n",
|
||
"too thin. The methods by which he reached his conclusions were a problem. They\n",
|
||
"involved a hocus-pocus of lines and areas on a chart. Intuition, and possibly\n",
|
||
"even mysticism, played a part. Those who employed rational, objective, and\n",
|
||
"scientific methods were naturally uneasy about Babson, although their methods\n",
|
||
"failed to foretell the crash. In these matters, as often in our culture, it is\n",
|
||
"far, far better to be wrong in a respectable way than to be right for the wrong\n",
|
||
"reasons.Wall Street was not at loss as to what to do about Babson. It promptly\n",
|
||
"and soundly denounced him. Barron's, in an editorial on September 9, referred to\n",
|
||
"him with heavy irony as the \"sage of Wellesley\" and said he should not be taken\n",
|
||
"seriously by anyone acquainted with the \"notorious inaccuracy\" of his past\n",
|
||
"statements. The stock exchange house of Hornblower and Weeks sternly told its\n",
|
||
"customers: \"We would not be stampeded into selling stocks because of a\n",
|
||
"gratuitous forecast of a bad break in the market by a well-known\n",
|
||
"statistician.\"22 Irving Fisher also took issue. He noted that dividends were\n",
|
||
"rising, that the suspicion of common stocks was receding, and that investment\n",
|
||
"trusts now offered the investor \"wide and well-managed diversification.\" His own\n",
|
||
"conclusion was: \"There may be a recession in stock prices, but not anything in\n",
|
||
"the nature of a crash.\" 23 Developing a slightly different theme, a Boston\n",
|
||
"investment trust told the public that it should be prepared for slight setbacks,\n",
|
||
"but it should realize that they would soon pass. In large advertisements it\n",
|
||
"pointed out that \"When temporary breaks come, indentations in the ever ascending\n",
|
||
"curve of American prosperity, individual stocks, even of the most successful\n",
|
||
"companies, go down with the general list...\" However, it also stated on its own\n",
|
||
"behalf that \"Incorporated Investors lands on a cushion.\" The Babson Break, as it\n",
|
||
"was promptly called, came on a Thursday. The market rallied on Friday and was\n",
|
||
"firm on Saturday. People seemed to be over their fear. It looked as though the\n",
|
||
"ever ascending curve would start up again, as so often before and Mr. Babson\n",
|
||
"notwithstanding. Then on the following week—the week of September 9—prices were\n",
|
||
"again ragged. On Monday, the Times, with the caution born of much premature\n",
|
||
"pessimism, hinted that the end had come and added, \"It is a well-known\n",
|
||
"characteristic of 'boom times' that the idea of their being terminated in the\n",
|
||
"old, unpleasant way is rarely recognized as possible.\" On Wednesday, in a fine\n",
|
||
"example of market prose, the Wall Street Journal observed that \"price movements\n",
|
||
"in the main body of stocks yesterday continued to display the characteristics of\n",
|
||
"a major advance temporarily halted for technical readjustment.\"The unevenness\n",
|
||
"continued. On some days the market was strong; on others it was weak. The\n",
|
||
"direction was slightly, erratically, but, viewed in retrospect, quite definitely\n",
|
||
"down. New investment trusts were still being formed; more speculators were\n",
|
||
"flocking to the market, and the volume of brokers' loans continued sharply up.\n",
|
||
"The end had come, but it was not yet in sight. Perhaps this was as well. The\n",
|
||
"last moments of life should be cherished, as Wall Street had been told. On\n",
|
||
"September 11, in keeping with a regular practice, The Wall Street Journal\n",
|
||
"printed its thought for the day. It was from Mark Twain. Don't part with your\n",
|
||
"illusions; when they are gone you may still exist, but you have ceased to live.\n",
|
||
"CHAPTER VThe CrashACCORDING TO the accepted view of events, by the autumn of\n",
|
||
"1929 the economy was well into a depression. In June the indexes of industrial\n",
|
||
"and of factory production both reached a peak and turned down. By October, the\n",
|
||
"Federal Reserve index of industrial production stood at 117 as compared with 126\n",
|
||
"four months earlier. Steel production declined from June on; in October freight-\n",
|
||
"car loadings fell. Home-building, a most mercurial industry, had been falling\n",
|
||
"for several years, and it slumped still farther in 1929. Finally, down came the\n",
|
||
"stock market. A penetrating student of the economic behavior of this period has\n",
|
||
"said that the market slump, \"reflected, in the main, the change which was\n",
|
||
"already apparent in the industrial situation.\" 1Thus viewed, the stock market is\n",
|
||
"but a mirror which, perhaps as in this instance, somewhat belatedly, provides an\n",
|
||
"image of the underlying or fundamental economic situation. Cause and effect run\n",
|
||
"from the economy to the stock market, never the reverse. In 1929 the economy was\n",
|
||
"headed for trouble. Eventually that trouble was violently reflected in Wall\n",
|
||
"Street.In 1929 there were good, or at least strategic, reasons for this view,\n",
|
||
"and it is easy to understand why it has become high doctrine. In Wall Street, as\n",
|
||
"elsewhere in 1929, few people wanted a bad depression. In Wall Street, as\n",
|
||
"elsewhere, there is deep faith in the power of incantation. When the market fell\n",
|
||
"many Wall Street citizens immediately sensed the real danger, which was that\n",
|
||
"income and employment—prosperity in general—would be adversely affected. This\n",
|
||
"had to be prevented. Preventive incantation required that as many important\n",
|
||
"people as possible repeat as firmly as they could that it wouldn't happen. This\n",
|
||
"they did. They explained how the stock market was merely the froth and that the\n",
|
||
"real substance of economic life rested in production, employment, and spending,\n",
|
||
"all of which would remain unaffected. No one knew for sure that this was so. As\n",
|
||
"an instrument of economic policy, incantation does not permit of minor doubts or\n",
|
||
"scruples. In the later years of depression it was important to continue\n",
|
||
"emphasizing the unimportance of the stock market. The depression was an\n",
|
||
"exceptionally disagreeable experience. Wall Street has not always been a\n",
|
||
"cherished symbol in our national life. In some of the devout regions of the\n",
|
||
"nation, those who speculate in stocks—the even more opprobrious term gamblers is\n",
|
||
"used—are not counted the greatest moral adornments of our society. Any\n",
|
||
"explanation of the depression which attributed importance to the market collapse\n",
|
||
"would accordingly have been taken very seriously, and it would have meant\n",
|
||
"serious trouble for Wall Street. Wall Street, no doubt, would have survived, but\n",
|
||
"there would have been scars. We should be clear that no deliberate conspiracy\n",
|
||
"existed to minimize the consequences of the Wall Street crash for the economy.\n",
|
||
"Rather, it merely appeared to everyone with an instinct for conservative\n",
|
||
"survival that Wall Street had better be kept out of it. It was vulnerable.In\n",
|
||
"fact, any satisfactory explanation of the events of the autumn of 1929 and\n",
|
||
"thereafter must accord a dignified role to the speculative boom and ensuing\n",
|
||
"collapse. Until September or October of 1929 the decline in economic activity\n",
|
||
"was very modest. As I shall argue later, until after the market crash one could\n",
|
||
"reasonably assume that this downward movement might soon reverse itself, as a\n",
|
||
"similar movement had reversed itself in 1927 or did subsequently in 1949. There\n",
|
||
"were no reasons for expecting disaster. No one could foresee that production,\n",
|
||
"prices, incomes, and all other indicators would continue to shrink through three\n",
|
||
"long and dismal years. Only after the market crash were there plausible grounds\n",
|
||
"to suppose that things might now for a long while get a lot worse. From the\n",
|
||
"foregoing it follows that the crash did not come—as some have suggested—because\n",
|
||
"the market suddenly became aware that a serious depression was in the offing. A\n",
|
||
"depression, serious or otherwise, could not be foreseen when the market fell.\n",
|
||
"There is still the possibility that the downturn in the indexes frightened the\n",
|
||
"speculators, led them to unload their stocks, and so punctured a bubble that had\n",
|
||
"in any case to be punctured one day. This is more plausible. Some people who\n",
|
||
"were watching the indexes may have been persuaded by this intelligence to sell,\n",
|
||
"and others may then have been encouraged to follow. This is not very important,\n",
|
||
"for it is in the nature of a speculative boom that almost anything can collapse\n",
|
||
"it. Any serious shock to confidence can cause sales by those speculators who\n",
|
||
"have always hoped to get out before the final collapse, but after all possible\n",
|
||
"gains from rising prices have been reaped. Their pessimism will infect those\n",
|
||
"simpler souls who had thought the market might go up forever but who now will\n",
|
||
"change their minds and sell. Soon there will be margin calls, and still others\n",
|
||
"will be forced to sell. So the bubble breaks.Along with the downturn of the\n",
|
||
"indexes Wall Street has always attributed importance to two other events in the\n",
|
||
"pricking of the bubble. In England on September 20, 1929, the enterprises of\n",
|
||
"Clarence Hatry suddenly collapsed. Hatry was one of those curiously un-English\n",
|
||
"figures with whom the English periodically find themselves unable to cope.\n",
|
||
"Although his earlier financial history had been anything but reassuring, Hatry\n",
|
||
"in the twenties had built up an industrial and financial empire of truly\n",
|
||
"impressive proportions. The nucleus, all the more remarkably, was a line of\n",
|
||
"coin-in-the-slot vending and automatic photograph machines. From these\n",
|
||
"unprepossessing enterprises he had marched on into investment trusts and high\n",
|
||
"finance. His expansion owed much to the issuance of unauthorized stock, the\n",
|
||
"increase of assets by the forging of stock certificates, and other equally\n",
|
||
"informal financing. In the lore of 1929, the unmasking of Hatry in London is\n",
|
||
"supposed to have struck a sharp blow to confidence in New York.2 Ranking with\n",
|
||
"Hatry in this lore was the refusal on October 11 of the Massachusetts Department\n",
|
||
"of Public Utilities to allow Boston Edison to split its stock four to one. As\n",
|
||
"the company argued, such split-ups were much in fashion. To avoid going along\n",
|
||
"was to risk being considered back in the corporate gaslight era. The refusal was\n",
|
||
"unprecedented. Moreover, the Department added insult to injury by announcing an\n",
|
||
"investigation of the company's rates and by suggesting that the present value of\n",
|
||
"the stock, \"due to the action of speculators,\" had reached a level where \"no\n",
|
||
"one, in our judgment ... on the basis of its earnings, would find it to his\n",
|
||
"advantage to buy it.\"These were uncouth words. They could have been important\n",
|
||
"as, conceivably, could have been the exposure of Clarence Hatry. But it could\n",
|
||
"also be that the inherently unstable equilibrium was shattered simply by a\n",
|
||
"spontaneous decision to get out. On September 22, the financial pages of the New\n",
|
||
"York papers carried an advertisement of an investment service with the arresting\n",
|
||
"headline, OVERSTAYING A BULL MARKET. Its message read as follows: \"Most\n",
|
||
"investors make money in a bull market, only to lose all profits made—and\n",
|
||
"sometimes more—in the readjustment that inevitably follows.\" Instead of the\n",
|
||
"downturn in the Federal Reserve industrial index, the exposure of Hatry, or the\n",
|
||
"unnatural obstinacy of the Massachusetts Department of Public Utilities, it\n",
|
||
"could have been such thoughts stirring first in dozens and then in hundreds, and\n",
|
||
"finally in thousands of breasts which finally brought an end to the boom. What\n",
|
||
"first stirred these doubts we do not know, but neither is it very important that\n",
|
||
"we know. IIConfidence did not disintegrate at once. As noted, through\n",
|
||
"September and into October, although the trend of the market was generally down,\n",
|
||
"good days came with the bad. Volume was high. On the New York Stock Exchange\n",
|
||
"sales were nearly always above four million, and frequently above five. In\n",
|
||
"September new issues appeared in even greater volume than in August, and they\n",
|
||
"regularly commanded a premium over the offering price. On September 20 the Times\n",
|
||
"noted that the stock of the recently launched Lehman Corporation which had been\n",
|
||
"offered at $104 had sold the day before at $136. (In the case of this well-\n",
|
||
"managed investment trust the public enthusiasm was not entirely misguided.)\n",
|
||
"During September brokers' loans increased by nearly $670 million, by far the\n",
|
||
"largest increase of any month to date. This showed that speculative zeal had not\n",
|
||
"diminished.Other signs indicated that the gods of the New Era were still in\n",
|
||
"their temples. In its October 12 issue, the Saturday Evening Post had a lead\n",
|
||
"story by Isaac F. Marcosson on Ivar Kreuger. This was a scoop, for Kreuger had\n",
|
||
"previously been inaccessible to journalists. \"Kreuger,\" Marcosson observed,\n",
|
||
"\"like Hoover, is an engineer. He has consistently applied engineer precision to\n",
|
||
"the welding of his far-flung industry.\" And this was not the only resemblance.\n",
|
||
"\"Like Hoover,\" the author added, \"Kreuger rules through pure reason.\" In the\n",
|
||
"interview Kreuger was remarkably candid on one point. He told Mr. Marcosson:\n",
|
||
"\"Whatever success I have had may perhaps be attributable to three things: One is\n",
|
||
"silence, the second is more silence, while the third is still more silence.\"\n",
|
||
"This was so. Two and a half years later Kreuger committed suicide in his Paris\n",
|
||
"apartment, and shortly thereafter it was discovered that his aversion to\n",
|
||
"divulging information, especially if accurate, had kept even his most intimate\n",
|
||
"acquaintances in ignorance of the greatest fraud in history. His American\n",
|
||
"underwriters, the eminently respectable firm of Lee, Higginson and Company of\n",
|
||
"Boston, had heard nothing and knew nothing. One of the members of the firm,\n",
|
||
"Donald Durant, was a member of the board of directors of the Kreuger\n",
|
||
"enterprises. He had never attended a directors' meeting, and it is certain that\n",
|
||
"he would have been no wiser had he done so.During the last weeks of October,\n",
|
||
"Time Magazine, young and not yet omniscient, also featured Kreuger on its\n",
|
||
"cover—\"a great admirer of Cecil Rhodes.\" Then a week later, as though to\n",
|
||
"emphasize its faith in the New Era, it went on to Samuel Insull. (A fortnight\n",
|
||
"after that, its youthful illusions shattered, the weekly newsmagazine gave the\n",
|
||
"place of historic honor to Warden Lawes of Sing Sing.) In these same Indian\n",
|
||
"summer days, The Wall Street Journal took notice of the official announcement\n",
|
||
"that Andrew Mellon would remain in the cabinet at least until 1933 (there had\n",
|
||
"been rumors that he might resign) and observed: \"Optimism again prevails ... the\n",
|
||
"announcement ... did more to restore confidence than anything else.\" In Germany\n",
|
||
"Charles E. Mitchell announced that the \"industrial condition of the United\n",
|
||
"States is absolutely sound,\" that too much attention was being paid to brokers'\n",
|
||
"loans, and that \"nothing can arrest the upward movement.\" On October 15, as he\n",
|
||
"sailed for home, he enlarged on the point: \"The markets generally are now in a\n",
|
||
"healthy condition ... values have a sound basis in the general prosperity of our\n",
|
||
"country.\" That same evening Professor Irving Fisher made his historic\n",
|
||
"announcement about the permanently high plateau and added, \"I expect to see the\n",
|
||
"stock market a good deal higher than it is today within a few months.\" Indeed,\n",
|
||
"the only disturbing thing, in these October days, was the fairly steady downward\n",
|
||
"drift in the market. IIIOn Saturday, October 19, Washington dispatches\n",
|
||
"reported that Secretary of Commerce Lamont was having trouble finding the\n",
|
||
"$100,000 in public funds that would be required to pay the upkeep of the yacht\n",
|
||
"Corsair which J. P. Morgan had just given the government. (Morgan's deprivation\n",
|
||
"was not extreme: a new $3,000,000 Corsair was being readied at Bath, Maine.)\n",
|
||
"There were other and more compelling indications of an unaccustomed stringency.\n",
|
||
"The papers told of a very weak market the day before—there were heavy declines\n",
|
||
"on late trading, and the Times industrial average had dropped about 7 points.\n",
|
||
"Steel had lost 7 points; General Electric, Westinghouse, and Montgomery Ward all\n",
|
||
"lost 6. Meanwhile, that day's market was behaving very badly. In the second\n",
|
||
"heaviest Saturday's trading in history, 3,488,100 shares were changing hands. At\n",
|
||
"the close the Times industrials were down 12 points. The blue chips were\n",
|
||
"seriously off, and speculative favorites had gone into a nosedive. J. I. Case,\n",
|
||
"for example, had fallen a full 40 points. On Sunday the market was front-page\n",
|
||
"news—the Times headline read, \"Stocks driven down as wave of selling engulfs the\n",
|
||
"market,\" and the financial editor next day reported for perhaps the tenth time\n",
|
||
"that the end had come. (He had learned, however, to hedge. \"For the time at any\n",
|
||
"rate,\" he said, \"Wall Street seemed to see the reality of things.\") No immediate\n",
|
||
"explanation of the break was forthcoming. The Federal Reserve had long been\n",
|
||
"quiet. Babson had said nothing new. Hatry and the Massachusetts Department of\n",
|
||
"Public Utilities were from a week to a month in the past. They became\n",
|
||
"explanations only later.The papers that Sunday carried three comments which were\n",
|
||
"to become familiar in the days that followed. After Saturday's trading, it was\n",
|
||
"noted, quite a few margin calls went out. This meant that the value of stock\n",
|
||
"which the recipients held on margin had declined to the point where it was no\n",
|
||
"longer sufficient collateral for the loan that had paid for it. The speculator\n",
|
||
"was being asked for more cash.The other two observations were more reassuring.\n",
|
||
"The papers agreed, and this was also the informed view on Wall Street, that the\n",
|
||
"worst was over. And it was predicted that on the following day the market would\n",
|
||
"begin to receive organized support. Weakness, should it appear, would be\n",
|
||
"tolerated no longer.Never was there a phrase with more magic than \"organized\n",
|
||
"support.\" Almost immediately it was on every tongue and in every news story\n",
|
||
"about the market. Organized support meant that powerful people would organize to\n",
|
||
"keep prices of stocks at a reasonable level. Opinions differed as to who would\n",
|
||
"organize this support. Some had in mind the big operators like Cutten, Durant,\n",
|
||
"and Raskob. They, of all people, couldn't afford a collapse. Some thought of the\n",
|
||
"bankers—Charles Mitchell had acted once before, and certainly if things got bad\n",
|
||
"he would act again. Some had in mind the investment trusts. They held huge\n",
|
||
"portfolios of common stocks, and obviously they could not afford to have them\n",
|
||
"become cheap. Also, they had cash. So if stocks did become cheap the investment\n",
|
||
"trusts would be in the market picking up bargains. This would mean that the\n",
|
||
"bargains wouldn't last. With so many people wanting to avoid a further fall, a\n",
|
||
"further fall would clearly be avoided.In the ensuing weeks the Sabbath pause had\n",
|
||
"a marked tendency to breed uneasiness and doubts and pessimism and a decision to\n",
|
||
"get out on Monday. This, it seems certain, was what happened on Sunday, October\n",
|
||
"20. IVMonday, October 21, was a very poor day. Sales totaled 6,091,870,\n",
|
||
"the third greatest volume in history, and some tens of thousands who were\n",
|
||
"watching the market throughout the country made a disturbing discovery. There\n",
|
||
"was no way of telling what was happening. Previously on big days of the bull\n",
|
||
"market the ticker had often fallen behind, and one didn't discover until well\n",
|
||
"after the market closed how much richer he had become. But the experience with a\n",
|
||
"falling market had been much more limited. Not since March had the ticker fallen\n",
|
||
"seriously behind on declining values. Many now learned for the first time that\n",
|
||
"they could be ruined, totally and forever, and not even know it. And if they\n",
|
||
"were not ruined there was a strong tendency to imagine it. From the opening on\n",
|
||
"the 21st the ticker lagged, and by noon it was an hour late. Not until an hour\n",
|
||
"and forty minutes after the close of the market did it record the last\n",
|
||
"transaction. Every ten minutes prices of selected stocks were printed on the\n",
|
||
"bond ticker, but the wide divergence between these and the prices on the tape\n",
|
||
"only added to the uneasiness—and to the growing conviction that it might be best\n",
|
||
"to sell. Things though bad were still not hopeless. Toward the end of Monday's\n",
|
||
"trading the market rallied and final prices were above the lows for the day. The\n",
|
||
"net losses were considerably less than on Saturday. Tuesday brought a somewhat\n",
|
||
"shaky gain. As so often before, the market seemed to be showing its ability to\n",
|
||
"come back. People got ready to record the experience as merely another setback\n",
|
||
"of which there had been so many previously.In doing so they were helped by the\n",
|
||
"two men who now were recognized as Wall Street's official prophets. On Monday in\n",
|
||
"New York, Professor Fisher said that the decline had represented only a \"shaking\n",
|
||
"out of the lunatic fringe.\" He went on to explain why he felt that the prices of\n",
|
||
"stocks during the boom had not caught up with their real value and would go\n",
|
||
"higher. Among other things, the market had not yet reflected the beneficent\n",
|
||
"effects of prohibition which had made the American worker \"more productive and\n",
|
||
"dependable.\"On Tuesday, Charles E. Mitchell dropped anchor in New York with the\n",
|
||
"observation that \"the decline had gone too far.\" (Time and sundry congressional\n",
|
||
"and court proceedings were to show that Mr. Mitchell had strong personal reasons\n",
|
||
"for feeling that way.) He added that conditions were \"fundamentally sound,\" said\n",
|
||
"again that too much attention had been paid to the large volume of brokers'\n",
|
||
"loans, and concluded that the situation was one which would correct itself if\n",
|
||
"left alone. However, another jarring suggestion came from Babson. He recommended\n",
|
||
"selling stocks and buying gold.By Wednesday, October 23, the effect of this\n",
|
||
"cheer was somehow dissipated. Instead of further gains there were heavy losses.\n",
|
||
"The opening was quiet enough, but toward midmorning motor accessory stocks were\n",
|
||
"sold heavily, and volume began to increase throughout the list. The last hour\n",
|
||
"was quite phenomenal—2,600,000 shares changed hands at rapidly declining prices.\n",
|
||
"The Times industrial average for the day dropped from 415 to 384, giving up all\n",
|
||
"of its gains since the end of the previous June. Tel and Tel lost 15 points;\n",
|
||
"General Electric, 20; Westinghouse, 25; and J. I. Case, another 46. Again the\n",
|
||
"ticker was far behind, and to add to the uncertainty an ice storm in the Middle\n",
|
||
"West caused widespread disruption of communications. That afternoon and evening\n",
|
||
"thousands of speculators decided to get out while—as they mistakenly\n",
|
||
"supposed—the getting was good. Other thousands were told they had no choice but\n",
|
||
"to get out unless they posted more collateral, for as the day's business came to\n",
|
||
"an end an unprecedented volume of margin calls went out. Speaking in Washington,\n",
|
||
"even Professor Fisher was fractionally less optimistic. He told a meeting of\n",
|
||
"bankers that \"security values in most instances were not inflated.\" However, he\n",
|
||
"did not weaken on the unrealized efficiencies of prohibition.The papers that\n",
|
||
"night went to press with a souvenir of a fast departing era. Formidable\n",
|
||
"advertisements announced subscription rights in a new offering of certificates\n",
|
||
"in Aktiebolaget Kreuger and Toll at $23. There was also one bit of cheer. It was\n",
|
||
"predicted that on the morrow the market would surely begin to receive \"organized\n",
|
||
"support.\" VThursday, October 24, is the first of the days which\n",
|
||
"history—such as it is on the subject—identifies with the panic of 1929. Measured\n",
|
||
"by disorder, fright, and confusion, it deserves to be so regarded. That day\n",
|
||
"12,894,650 shares changed hands, many of them at prices which shattered the\n",
|
||
"dreams and the hopes of those who had owned them. Of all the mysteries of the\n",
|
||
"stock exchange there is none so impenetrable as why there should be a buyer for\n",
|
||
"everyone who seeks to sell. October 24, 1929, showed that what is mysterious is\n",
|
||
"not inevitable. Often there were no buyers, and only after wide vertical\n",
|
||
"declines could anyone be induced to bid. The panic did not last all day. It was\n",
|
||
"a phenomenon of the morning hours. The market opening itself was unspectacular,\n",
|
||
"and for a while prices were firm. Volume, however, was very large, and soon\n",
|
||
"prices began to sag. Once again the ticker dropped behind. Prices fell farther\n",
|
||
"and faster, and the ticker lagged more and more. By eleven o'clock the market\n",
|
||
"had degenerated into a wild, mad scramble to sell. In the crowded boardrooms\n",
|
||
"across the country the ticker told of a frightful collapse. But the selected\n",
|
||
"quotations coming in over the bond ticker also showed that current values were\n",
|
||
"far below the ancient history of the tape. The uncertainty led more and more\n",
|
||
"people to try to sell. Others, no longer able to respond to margin calls, were\n",
|
||
"sold out. By eleven-thirty the market had surrendered to blind, relentless fear.\n",
|
||
"This, indeed, was panic.Outside the Exchange in Broad Street a weird roar could\n",
|
||
"be heard. A crowd gathered. Police Commissioner Grover Whalen became aware that\n",
|
||
"something was happening and dispatched a special police detail to Wall Street to\n",
|
||
"insure the peace. More people came and waited, though apparently no one knew for\n",
|
||
"what. A workman appeared atop one of the high buildings to accomplish some\n",
|
||
"repairs, and the multitude assumed he was a would-be suicide and waited\n",
|
||
"impatiently for him to jump. Crowds also formed around the branch offices of\n",
|
||
"brokerage firms throughout the city and, indeed, throughout the country. Word of\n",
|
||
"what was happening, or what was thought to be happening, was passed out by those\n",
|
||
"who were within sight of the board or the Trans-Lux. An observer thought that\n",
|
||
"people's expressions showed \"not so much suffering as a sort of horrified\n",
|
||
"incredulity.\" 3 Rumor after rumor swept Wall Street and these outlying wakes.\n",
|
||
"Stocks were now selling for nothing. The Chicago and Buffalo Exchanges had\n",
|
||
"closed. A suicide wave was in progress, and eleven well-known speculators had\n",
|
||
"already killed themselves.At twelve-thirty the officials of the New York Stock\n",
|
||
"Exchange closed the visitors gallery on the wild scenes below. One of the\n",
|
||
"visitors who had just departed was showing his remarkable ability to be on hand\n",
|
||
"with history. He was the former Chancellor of the Exchequer, Mr. Winston\n",
|
||
"Churchill. It was he who in 1925 returned Britain to the gold standard and the\n",
|
||
"overvalued pound. Accordingly, he was responsible for the strain which sent\n",
|
||
"Montagu Norman to plead in New York for easier money, which caused credit to be\n",
|
||
"eased at the fatal time, which, in this academy view, in turn caused the boom.\n",
|
||
"Now Churchill, it could be imagined, was viewing his awful handiwork.There is no\n",
|
||
"record of anyone's having reproached him. Economics was never his strong point,\n",
|
||
"so (and wisely) it seems most unlikely that he reproached himself. VIIn\n",
|
||
"New York at least the panic was over by noon. At noon the organized support\n",
|
||
"appeared. At twelve o'clock reporters learned that a meeting was convening at 23\n",
|
||
"Wall Street at the offices of J. P. Morgan and Company. The word quickly passed\n",
|
||
"as to who was there—Charles E. Mitchell, the Chairman of the Board of the\n",
|
||
"National City Bank, Albert H. Wiggin, the Chairman of the Chase National Bank,\n",
|
||
"William C. Potter, the President of the Guaranty Trust Company, Seward Prosser,\n",
|
||
"the Chairman of the Bankers Trust Company, and the host, Thomas W. Lamont, the\n",
|
||
"senior partner of Morgan's. According to legend, during the panic of 1907 the\n",
|
||
"elder Morgan had brought to a halt the discussion of whether to save the\n",
|
||
"tottering Trust Company of America by saying that the place to stop the panic\n",
|
||
"was there. It was stopped. Now, twenty-two years later, that drama was being re-\n",
|
||
"enacted. The elder Morgan was dead. His son was in Europe. But equally\n",
|
||
"determined men were moving in. They were the nation's most powerful financiers.\n",
|
||
"They had not yet been pilloried and maligned by New Dealers. The very news that\n",
|
||
"they would act would release people from the fear to which they had\n",
|
||
"surrendered.It did. A decision was quickly reached to pool resources to support\n",
|
||
"the market.4 The meeting broke up, and Thomas Lamont met with reporters. His\n",
|
||
"manner was described as serious, but his words were reassuring. In what\n",
|
||
"Frederick Lewis Allen later called one of the most remarkable understatements of\n",
|
||
"all time,5 he told the newspapermen, \"There has been a little distress selling\n",
|
||
"on the Stock Exchange.\" He added that this was \"due to a technical condition of\n",
|
||
"the market\" rather than any fundamental cause, and told the newsmen that things\n",
|
||
"were \"susceptible to betterment.\" The bankers, he let it be known, had decided\n",
|
||
"to better things. Word had already reached the floor of the Exchange that the\n",
|
||
"bankers were meeting, and the news ticker had spread the magic word afield.\n",
|
||
"Prices firmed at once and started to rise. Then at one-thirty Richard Whitney\n",
|
||
"appeared on the floor and went to the post where steel was traded. Whitney was\n",
|
||
"perhaps the best-known figure on the floor. He was one of the group of men of\n",
|
||
"good background and appropriate education who, in that time, were expected to\n",
|
||
"manage the affairs of the Exchange. Currently he was vice-president of the\n",
|
||
"Exchange, but in the absence of E. H. H. Simmons in Hawaii he was serving as\n",
|
||
"acting president. What was much more important at the moment, he was known as\n",
|
||
"floor trader for Morgan's and, indeed, his older brother was a Morgan partner.As\n",
|
||
"he made his way through the teeming crowd, Whitney appeared debonair and self-\n",
|
||
"confident—some later described his manner as jaunty. (His own firm dealt largely\n",
|
||
"in bonds, so it is improbable that he had been much involved in the turmoil of\n",
|
||
"the morning.) At the Steel post he bid 205 for 10,000 shares. This was the price\n",
|
||
"of the last sale, and the current bids were several points lower. In an\n",
|
||
"operation that was totally devoid of normal commercial reticence, he got 200\n",
|
||
"shares and then left the rest of the order with the specialist. He continued on\n",
|
||
"his way, placing similar orders for fifteen or twenty other stocks.This was it.\n",
|
||
"The bankers, obviously, had moved in. The effect was electric. Fear vanished and\n",
|
||
"gave way to concern lest the new advance be missed. Prices boomed upward.The\n",
|
||
"bankers had, indeed, brought off a notable coup. Prices as they fell that\n",
|
||
"morning kept crossing a large volume of stop-loss orders—orders calling for\n",
|
||
"sales whenever a specified price was reached. Brokers had placed many of these\n",
|
||
"orders for their own protection on the securities of customers who had not\n",
|
||
"responded to calls for additional margin. Each of these stop-loss orders tripped\n",
|
||
"more securities into the market and drove prices down farther. Each spasm of\n",
|
||
"liquidation thus insured that another would follow. It was this literal chain\n",
|
||
"reaction which the bankers checked, and they checked it decisively. In the\n",
|
||
"closing hour, selling orders continuing to come in from across the country\n",
|
||
"turned the market soft once more. Still, in its own way, the recovery on Black\n",
|
||
"Thursday was as remarkable as the selling that made it so black. The Times\n",
|
||
"industrials were off only 12 points, or a little more than a third of the loss\n",
|
||
"of the previous day. Steel, the stock that Whitney had singled out to start the\n",
|
||
"recovery, had opened that morning at 205½, a point or two above the previous\n",
|
||
"close. At the lowest it was down to 193½ for a 12-point loss.6 Then it recovered\n",
|
||
"to close at 206 for a surprising net gain of 2 points for this day. Montgomery\n",
|
||
"Ward, which had opened at 83 and gone to 50, came back to 74. General Electric\n",
|
||
"was at one point 32 points below its opening price and then came back 25 points.\n",
|
||
"On the Curb, Goldman Sachs Trading Corporation opened at 81, dropped to 65, and\n",
|
||
"then came back to 80. J. I. Case, maintaining a reputation for eccentric\n",
|
||
"behavior that had brought much risk capital into the threshing machine business,\n",
|
||
"made a net gain of 7 points for the day. Many had good reason to be grateful to\n",
|
||
"the financial leaders of Wall Street. VIINot everyone could be grateful\n",
|
||
"to be sure. Across the country people were only dimly aware of the improvement.\n",
|
||
"By early afternoon, when the market started up, the ticker was hours behind.\n",
|
||
"Although the spot quotations on the bond ticker showed the improvement, the\n",
|
||
"ticker itself continued to grind out the most dismal of news. And the news on\n",
|
||
"the ticker was what counted. To many, many watchers it meant that they had been\n",
|
||
"sold out and that their dream—in fact, their brief reality—of opulence had gone\n",
|
||
"glimmering, together with home, car, furs, jewelry, and reputation. That the\n",
|
||
"market, after breaking them, had recovered was the most chilling of comfort.It\n",
|
||
"was eight and a half minutes past seven that night before the ticker finished\n",
|
||
"recording the day's misfortunes. In the boardrooms speculators who had been sold\n",
|
||
"out since morning sat silently watching the tape. The habit of months or years,\n",
|
||
"however idle it had now become, could not be abandoned at once. Then, as the\n",
|
||
"final trades were registered, sorrowfully or grimly, according to their nature,\n",
|
||
"they made their way out into the gathering night.In Wall Street itself lights\n",
|
||
"blazed from every office as clerks struggled to come abreast of the day's\n",
|
||
"business. Messengers and boardroom boys, caught up in the excitement and\n",
|
||
"untroubled by losses, went skylarking through the streets until the police\n",
|
||
"arrived to quell them. Representatives of thirty-five of the largest wire houses\n",
|
||
"assembled at the offices of Hornblower and Weeks and told the press on departing\n",
|
||
"that the market was \"fundamentally sound\" and \"technically in better condition\n",
|
||
"than it has been in months.\" It was the unanimous view of those present that the\n",
|
||
"worst had passed. The host firm dispatched a market letter which stated that\n",
|
||
"\"commencing with today's trading the market should start laying the foundation\n",
|
||
"for the constructive advance which we believe will characterize 1930.\" Charles\n",
|
||
"E. Mitchell announced that the trouble was \"purely technical\" and that\n",
|
||
"\"fundamentals remained unimpaired.\" Senator Carter Glass said the trouble was\n",
|
||
"due largely to Charles E. Mitchell. Senator Wilson of Indiana attributed the\n",
|
||
"crash to Democratic resistance to a higher tariff. VIIIOn Friday and\n",
|
||
"Saturday trading continued heavy—just under six million on Friday and over two\n",
|
||
"million at the short session on Saturday. Prices, on the whole, were steady—the\n",
|
||
"averages were a trifle up on Friday but slid off on Saturday. It was thought\n",
|
||
"that the bankers were able to dispose of most of the securities they had\n",
|
||
"acquired while shoring up the market on Thursday. Not only were things better,\n",
|
||
"but everyone was clear as to who had made them so. The bankers had shown both\n",
|
||
"their coin-age and their power, and the people applauded warmly and generously.\n",
|
||
"The financial community, the Times said, now felt \"secure in the knowledge that\n",
|
||
"the most powerful banks in the country stood ready to prevent a recurrence [of\n",
|
||
"panic].\" As a result it had \"relaxed its anxiety.\"Perhaps never before or since\n",
|
||
"have so many people taken the measure of economic prospects and found them so\n",
|
||
"favorable as in the two days following the Thursday disaster. The optimism even\n",
|
||
"included a note of self-congratulation. Colonel Ayres in Cleveland thought that\n",
|
||
"no other country could have come through such a bad crash so well. Others\n",
|
||
"pointed out that the prospects for business were good and that the stock market\n",
|
||
"debacle would not make them any less favorable. No one knew, but it cannot be\n",
|
||
"stressed too frequently, that for effective incantation knowledge is neither\n",
|
||
"necessary nor assumed. Eugene M. Stevens, the President of the Continental\n",
|
||
"Illinois Bank, said, \"There is nothing in the business situation to justify any\n",
|
||
"nervousness.\" Walter Teagle said there had been no \"fundamental change\" in the\n",
|
||
"oil business to justify concern; Charles M. Schwab said that the steel business\n",
|
||
"had been making \"fundamental progress\" toward stability and added that this\n",
|
||
"\"fundamentally sound condition\" was responsible for the prosperity of the\n",
|
||
"industry; Samuel Vauclain, Chairman of the Baldwin Locomotive Works, declared\n",
|
||
"that \"fundamentals are sound\"; President Hoover said that \"the fundamental\n",
|
||
"business of the country, that is production and distribution of commodities, is\n",
|
||
"on a sound and prosperous basis.\" President Hoover was asked to say something\n",
|
||
"more specific about the market—for example, that stocks were now cheap—but he\n",
|
||
"refused.7Many others joined in. Howard C. Hopson, the head of Associated Gas and\n",
|
||
"Electric, omitted the standard reference to fundamentals and thought it was\n",
|
||
"\"undoubtedly beneficial to the business interests of the country to have the\n",
|
||
"gambling type of speculator eliminated.\" (Mr. Hopson, himself a speculator,\n",
|
||
"although more of the sure-thing type, was also eliminated in due course.) A\n",
|
||
"Boston investment trust took space in The Wall Street Journal to say, \"S-T-E-A-\n",
|
||
"D-Y Everybody! Calm thinking is in order. Heed the words of America's greatest\n",
|
||
"bankers.\" A single dissonant note, though great in portent, went unnoticed.\n",
|
||
"Speaking in Poughkeepsie, Governor Franklin D. Roosevelt criticized the \"fever\n",
|
||
"of speculation.\" On Sunday there were sermons suggesting that a certain measure\n",
|
||
"of divine retribution had been visited on the Republic and that it had not been\n",
|
||
"entirely unmerited. People had lost sight of spiritual values in their single-\n",
|
||
"minded pursuit of riches. Now they had had their lesson.Almost everyone believed\n",
|
||
"that the heavenly knuckle-rapping was over and that speculation could be now\n",
|
||
"resumed in earnest. The papers were full of the prospects for next week's\n",
|
||
"market.Stocks, it was agreed, were again cheap and accordingly there would be a\n",
|
||
"heavy rush to buy. Numerous stories from the brokerage houses, some of them\n",
|
||
"possibly inspired, told of a fabulous volume of buying orders which was piling\n",
|
||
"up in anticipation of the opening of the market. In a concerted advertising\n",
|
||
"campaign in Monday's papers, stock market firms urged the wisdom of picking up\n",
|
||
"these bargains promptly. \"We believe,\" said one house, \"that the investor who\n",
|
||
"purchases securities at this time with the discrimination that is always a\n",
|
||
"condition of prudent investing, may do so with utmost confidence.\" On Monday the\n",
|
||
"real disaster began. CHAPTER VIThings Become More SeriousIN THE AUTUMN\n",
|
||
"of 1929 the New York Stock Exchange, under roughly its present constitution, was\n",
|
||
"112 years old. During this lifetime it had seen some difficult days. On\n",
|
||
"September 18, 1873, the firm of Jay Cooke and Company failed, and, as a more or\n",
|
||
"less direct result, so did fifty-seven other stock exchange firms in the next\n",
|
||
"few weeks. On October 23, 1907, call money rates reached 125 per cent in the\n",
|
||
"panic of that year. On September 16, 1920—the autumn months are the off season\n",
|
||
"in Wall Street—a bomb exploded in front of Morgan's next door, killing thirty\n",
|
||
"people and injuring a hundred more.A common feature of all these earlier\n",
|
||
"troubles was that having happened they were over. The worst was reasonably\n",
|
||
"recognizable as such. The singular feature of the great crash of 1929 was that\n",
|
||
"the worst continued to worsen. What looked one day like the end proved on the\n",
|
||
"next day to have been only the beginning. Nothing could have been more\n",
|
||
"ingeniously designed to maximize the suffering, and also to insure that as few\n",
|
||
"as possible escaped the common misfortune. The fortunate speculator who had\n",
|
||
"funds to answer the first margin call presently got another and equally urgent\n",
|
||
"one, and if he met that there would still be another. In the end all the money\n",
|
||
"he had was extracted from him and lost. The man with the smart money, who was\n",
|
||
"safely out of the market when the first crash came, naturally went back in to\n",
|
||
"pick up bargains. (Not only were a recorded 12,894,650 shares sold on October\n",
|
||
"24; precisely the same number were bought.) The bargains then suffered a ruinous\n",
|
||
"fall. Even the man who waited out all of October and all of November, who saw\n",
|
||
"the volume of trading return to normal and saw Wall Street become as placid as a\n",
|
||
"produce market, and who then bought common stocks would see their value drop to\n",
|
||
"a third or a fourth of the purchase price in the next twenty-four months. The\n",
|
||
"Coolidge bull market was a remarkable phenomenon. The ruthlessness of its\n",
|
||
"liquidation was, in its own way, equally remarkable. IIMonday, October\n",
|
||
"28, was the first day on which this process of climax and anticlimax ad\n",
|
||
"infinitum began to reveal itself. It was another terrible day. Volume was huge,\n",
|
||
"although below the previous Thursday—nine and a quarter million shares as\n",
|
||
"compared with nearly thirteen. But the losses were far more severe. The Times\n",
|
||
"industrials were down 49 points for the day. General Electric was off 48;\n",
|
||
"Westinghouse, 34; Tel and Tel, 34. Steel went down 18 points. Indeed, the\n",
|
||
"decline on this one day was greater than that of all the preceding week of\n",
|
||
"panic. Once again a late ticker left everyone in ignorance of what was\n",
|
||
"happening, save that it was bad.On this day there was no recovery. At one-ten\n",
|
||
"Charles E. Mitchell was observed going into Morgan's, and the news ticker\n",
|
||
"carried the magic word. Steel rallied and went from 194 to 198. But Richard\n",
|
||
"Whitney did not materialize. It seems probable in light of later knowledge that\n",
|
||
"Mitchell was on the way to float a personal loan. The market weakened again, and\n",
|
||
"in the last hour a phenomenal three million shares—a big day's business before\n",
|
||
"and ever since—changed hands at rapidly falling prices. At four-thirty in the\n",
|
||
"afternoon the bankers assembled once more at Morgan's, and they remained in\n",
|
||
"session until six-thirty. They were described as taking a philosophical\n",
|
||
"attitude, and they told the press that the situation \"retained hopeful\n",
|
||
"features,\" although these were not specified. But the statement they released\n",
|
||
"after the meeting made clear what had been discussed for the two hours. It was\n",
|
||
"no part of the bankers' purpose, the statement said, to maintain any particular\n",
|
||
"level of prices or to protect anyone's profit. Rather the aim was to have an\n",
|
||
"orderly market, one in which offers would be met by bids at some price. The\n",
|
||
"bankers were only concerned that \"air holes,\" as Mr. Lamont had dubbed them, did\n",
|
||
"not appear.Like many lesser men, Mr. Lamont and his colleagues had suddenly\n",
|
||
"found themselves overcommitted on a falling market. The time had come to go\n",
|
||
"short on promises. Support, organized or otherwise, could not contend with the\n",
|
||
"overwhelming, pathological desire to sell. The meeting had considered how to\n",
|
||
"liquidate the commitment to support the market without adding to the public\n",
|
||
"perturbation.The formula that was found was a chilling one. On Thursday, Whitney\n",
|
||
"had supported prices and protected profits—or stopped losses. This was what\n",
|
||
"people wanted. To the man who held stock on margin, disaster had only one face\n",
|
||
"and that was falling prices. But now prices were to be allowed to fall. The\n",
|
||
"speculator's only comfort, henceforth, was that his ruin would be accomplished\n",
|
||
"in an orderly and becoming manner.There were no recriminations at the time. Our\n",
|
||
"political life favors the extremes of speech; the man who is gifted in the arts\n",
|
||
"of abuse is bound to be a notable, if not always a great figure. In business\n",
|
||
"things are different. Here we are surprisingly gentle and forbearing. Even\n",
|
||
"preposterous claims or excuses are normally taken, at least for all public\n",
|
||
"purposes, at their face value. On the evening of the 28th no one any longer\n",
|
||
"could feel \"secure in the knowledge that the most powerful banks stood ready to\n",
|
||
"prevent a recurrence\" of panic. The market had reasserted itself as an\n",
|
||
"impersonal force beyond the power of any person to control, and, while this is\n",
|
||
"the way markets are supposed to be, it was horrible. But no one assailed the\n",
|
||
"bankers for letting the people down. There was even some talk that on the next\n",
|
||
"day the market might receive organized support. IIITuesday, October 29,\n",
|
||
"was the most devastating day in the history of the New York stock market, and it\n",
|
||
"may have been the most devastating day in the history of markets. It combined\n",
|
||
"all of the bad features of all of the bad days before. Volume was immensely\n",
|
||
"greater than on Black Thursday; the drop in prices was almost as great as on\n",
|
||
"Monday. Uncertainty and alarm were as great as on either.Selling began as soon\n",
|
||
"as the market opened and in huge volume. Great blocks of stock were offered for\n",
|
||
"what they would bring; in the first half hour sales were at a 33,000,000-a-day\n",
|
||
"rate. The air holes, which the bankers were to close, opened wide. Repeatedly\n",
|
||
"and in many issues there was a plethora of selling orders and no buyers at all.\n",
|
||
"The stock of White Sewing Machine Company, which had reached a high of 48 in the\n",
|
||
"months preceding, had closed at 11 the night before. During the day\n",
|
||
"someone—according to Frederick Lewis Allen it was thought to have been a bright\n",
|
||
"messenger boy for the Exchange—had the happy idea of entering a bid for a block\n",
|
||
"of stock at a dollar a share. In the absence of any other bid he got it.1 Once\n",
|
||
"again, of course, the ticker lagged—at the close it was two and a half hours\n",
|
||
"behind. By then, 16,410,030 sales had been recorded on the New York Stock\n",
|
||
"Exchange—some certainly went unrecorded—or more than three times the number that\n",
|
||
"was once considered a fabulously big day. The Times industrial averages were\n",
|
||
"down 43 points, canceling all of the gains of the twelve wonderful months\n",
|
||
"preceding. The losses would have been worse had there not been a closing rally.\n",
|
||
"Thus Steel, for which Whitney had bid 205 on Thursday, reached 167 during the\n",
|
||
"course of the day, although it rallied to 174 at the close. American Can opened\n",
|
||
"at 130, dropped to 110, and rose to 120. Westinghouse opened at 131—on September\n",
|
||
"3 it had closed at 286—and dropped to 100. Then it rallied to 126. But the worst\n",
|
||
"thing that happened on this terrible day was to the investment trusts. Not only\n",
|
||
"did they go down, but it became apparent that they could go practically to\n",
|
||
"nothing. Goldman Sachs Trading Corporation had closed at 60 the night before.\n",
|
||
"During the day it dropped to 35 and closed at that level, off by not far short\n",
|
||
"of half. Blue Ridge, its offspring once removed, on which the magic of leverage\n",
|
||
"was now working in reverse, did much worse. Early in September it had sold at\n",
|
||
"24. By October 24 it was down to 12, but it resisted rather well the misfortunes\n",
|
||
"of that day and the day following. On the morning of October 29 it opened at 10\n",
|
||
"and promptly slipped to 3, giving up more than two thirds of its value. It\n",
|
||
"recovered later but other investment trusts did less well; their stock couldn't\n",
|
||
"be sold at all.The worst day on Wall Street came eventually to an end. Once\n",
|
||
"again the lights blazed all night. Members of the Exchange, their employees, and\n",
|
||
"the employees of the Stock Exchange by now were reaching the breaking point from\n",
|
||
"strain and fatigue. In this condition they faced the task of recording and\n",
|
||
"handling the greatest volume of transactions ever. All of this was without the\n",
|
||
"previous certainty that things might get better. They might go on getting worse.\n",
|
||
"In one house an employee fainted from exhaustion, was revived and put back to\n",
|
||
"work again. IVIn the first week the slaughter had been of the innocents.\n",
|
||
"During this second week there is some evidence that it was the well-to-do and\n",
|
||
"the wealthy who were being subjected to a leveling process comparable in\n",
|
||
"magnitude and suddenness to that presided over a decade before by Lenin. The\n",
|
||
"size of the blocks of stock which were offered suggested that big speculators\n",
|
||
"were selling or being sold. Another indication came from the boardrooms. A week\n",
|
||
"before they were crowded, now they were nearly empty. Those now in trouble had\n",
|
||
"facilities for suffering in private.The bankers met twice on the 29th—at noon\n",
|
||
"and again in the evening. There was no suggestion that they were philosophical.\n",
|
||
"This was hardly remarkable because, during the day, an appalling rumor had swept\n",
|
||
"the Exchange. It was that the bankers' pool, so far from stabilizing the market,\n",
|
||
"was actually selling stocks! The prestige of the bankers had in truth been\n",
|
||
"falling even more rapidly than the market. After the evening session, Mr. Lamont\n",
|
||
"met the press with the disenchanting task of denying that they had been\n",
|
||
"liquidating securities—or participating in a bear raid. After explaining again,\n",
|
||
"somewhat redundantly in view of the day's events, that it was not the purpose of\n",
|
||
"the bankers to maintain a particular level of prices, he concluded: \"The group\n",
|
||
"has continued and will continue in a co-operative way to support the market and\n",
|
||
"has not been a seller of stocks.\" In fact, as later intelligence revealed,\n",
|
||
"Albert H. Wiggin of the Chase was personally short at the time to the time of\n",
|
||
"some millions. His co-operative support, which if successful would have cost him\n",
|
||
"heavily, must have had an interesting element of ambivalence. So ended the\n",
|
||
"organized support. The phrase recurred during the next few days, but no one\n",
|
||
"again saw in it any ground for hope. Few men ever lost position so rapidly as\n",
|
||
"did the New York bankers in the five days from October 24 to October 29. The\n",
|
||
"crash on October 24 was the signal for corporations and out-of-town banks, which\n",
|
||
"had been luxuriating in the 10 per cent and more rate of interest, to recall\n",
|
||
"their money from Wall Street. Between October 23 and October 30, as values fell\n",
|
||
"and margin accounts were liquidated, the volume of brokers' loans fell by over a\n",
|
||
"billion. But the corporations and the out-of-town banks responded to the\n",
|
||
"horrifying news from New York—although, in fact, their funds were never\n",
|
||
"seriously endangered—by calling home over two billions. The New York banks\n",
|
||
"stepped into the gaping hole that was left by these summer financiers, and\n",
|
||
"during that first week of crisis they increased their loans by about a billion.\n",
|
||
"This was a bold step. Had the New York banks succumbed to the general fright, a\n",
|
||
"money panic would have been added to the other woes. Stocks would have been\n",
|
||
"dumped because their owners could not have borrowed money at any price to carry\n",
|
||
"them. To prevent this was a considerable achievement for which all who owned\n",
|
||
"stocks should have been thankful. But the banks received no credit. People\n",
|
||
"remembered only that they had bravely undertaken to stem the price collapse and\n",
|
||
"had failed.Despite a flattering supposition to the contrary, people come readily\n",
|
||
"to terms with power. There is little reason to think that the power of the great\n",
|
||
"bankers, while they were assumed to have it, was much resented. But as the\n",
|
||
"ghosts of numerous tyrants, from Julius Caesar to Benito Mussolini will testify,\n",
|
||
"people are very hard on those who, having had power, lose it or are destroyed.\n",
|
||
"Then anger at past arrogance is joined with contempt for present weakness. The\n",
|
||
"victim or his corpse is made to suffer all available indignities.Such was the\n",
|
||
"fate of the bankers. For the next decade they were fair game for congressional\n",
|
||
"committees, courts, the press, and the comedians. The great pretensions and the\n",
|
||
"great failures of these days were a cause. A banker need not be popular; indeed,\n",
|
||
"a good banker in a healthy capitalist society should probably be much disliked.\n",
|
||
"People do not wish to trust their money to a hail-fellow-well-met but to a\n",
|
||
"misanthrope who can say no. However, a banker must not seem futile, ineffective,\n",
|
||
"or vaguely foolish. In contrast with the stern power of Morgan in 1907, that was\n",
|
||
"precisely how his successors seemed, or were made to seem, in 1929.The failure\n",
|
||
"of the bankers did not leave the community entirely without constructive\n",
|
||
"leadership. There was Mayor James J. Walker. Appearing before a meeting of\n",
|
||
"motion picture exhibitors on that Tuesday, he appealed to them to \"show pictures\n",
|
||
"which will reinstate courage and hope in the hearts of the people.\" VOn\n",
|
||
"the Exchange itself, there was a strong feeling that corn-age and hope might\n",
|
||
"best be restored by just closing up for a while. This feeling had, in fact, been\n",
|
||
"gaining force for several days. Now it derived support from the simple\n",
|
||
"circumstance that everyone was badly in need of sleep. Employees of some Stock\n",
|
||
"Exchange firms had not been home for days. Hotel rooms in downtown New York were\n",
|
||
"at a premium, and restaurants in the financial area had gone on to a fifteen-\n",
|
||
"and twenty-hour day. Nerves were bad, and mistakes were becoming increasingly\n",
|
||
"common. After the close of trading on Tuesday, a broker found a large waste\n",
|
||
"basket of unexecuted orders which he had set aside for early attention and had\n",
|
||
"totally forgotten.2 One customer, whose margin account was impaired, was sold\n",
|
||
"out twice. A number of firms needed some time to see if they were still solvent.\n",
|
||
"There were, in fact, no important failures by Stock Exchange firms during these\n",
|
||
"days, although one firm had reported itself bankrupt as the result of a clerical\n",
|
||
"error by an employee who was in the last stages of fatigue.3 Yet to close the\n",
|
||
"Exchange was a serious matter. It might somehow signify that stocks had lost all\n",
|
||
"their value, with consequences no one could foresee. In any case, securities\n",
|
||
"would immediately become a badly frozen asset. This would be hard on the wholly\n",
|
||
"solvent investors who might need to realize on them or use them as collateral.\n",
|
||
"And sooner or later a new \"gutter\" market would develop in which individuals\n",
|
||
"would informally dispose of stocks to those increasingly exceptional individuals\n",
|
||
"who still wanted to buy them.In 1929 the New York Stock Exchange was in\n",
|
||
"principle a sovereignty of its members. Apart from the general statutes relating\n",
|
||
"to the conduct of business and the prevention of fraud, it was subject to no\n",
|
||
"important state or federal regulation. This meant a considerable exercise of\n",
|
||
"self-government. Legislation governing the conduct of trading had to be kept\n",
|
||
"under review and enforced. Stocks had to be approved for listing. The building\n",
|
||
"and other facilities of the Exchange had to be managed. As with the United\n",
|
||
"States Congress, most of this work was done in committees. (These, in turn, were\n",
|
||
"dominated by a somewhat smaller group of members who were expected and\n",
|
||
"accustomed to run things.) A decision to close the Exchange had to be taken by\n",
|
||
"the Governing Committee, a body of about forty members. The mere knowledge that\n",
|
||
"this body was meeting would almost certainly have an unfavorable effect on the\n",
|
||
"market. Nonetheless, at noon on Tuesday, the 29th, a meeting was held. The\n",
|
||
"members of the committee left the floor in twos and threes and went, not to the\n",
|
||
"regular meeting room, but to the office of the President of the Stock Clearing\n",
|
||
"Corporation directly below the trading floor. Some months later, Acting\n",
|
||
"President Whitney described the session with considerable graphic talent. \"The\n",
|
||
"office they met in was never designed for large meetings of this sort, with the\n",
|
||
"result that most of the Governors were compelled to stand, or to sit on tables.\n",
|
||
"As the meeting proceeded, panic was raging overhead on the floor. Every few\n",
|
||
"minutes the latest prices were announced, with quotations moving swiftly and\n",
|
||
"irresistibly downwards. The feeling of those present was revealed by their habit\n",
|
||
"of continually lighting cigarettes, taking a puff or two, putting them out and\n",
|
||
"lighting new ones—a practice which soon made the narrow room blue with smoke and\n",
|
||
"extremely stuffy.\"The result of these nervous deliberations was a decision to\n",
|
||
"meet again in the evening. By evening the late rally had occurred, and it was\n",
|
||
"decided to stay open for another day. The next day a further formula was hit\n",
|
||
"upon. The Exchange would stay open. But it would have some special holidays and\n",
|
||
"then go on short hours, and this would be announced just as soon as the market\n",
|
||
"seemed strong enough to stand it.Many still wanted to close. Whitney said later,\n",
|
||
"although no doubt with some exaggeration, that in the days to come \"the\n",
|
||
"authorities of the Exchange led the life of hunted things, until [eventually]\n",
|
||
"the desirability of holding the market open became apparent to all.\"\n",
|
||
"VIThe next day those forces were at work which on occasion bring salvation\n",
|
||
"precisely when salvation seems impossible. Stocks rose wonderfully,\n",
|
||
"miraculously, though still on enormous volume. The Times industrials were up 31\n",
|
||
"points for the day, thus recouping a large part of the terrible losses of the\n",
|
||
"day before. Why this recovery occurred no one will ever know. Organized support\n",
|
||
"can have no credit. Organized reassurance has a somewhat better claim. On the\n",
|
||
"evening of the 29th, Dr. Julius Klein, Assistant Secretary of Commerce, friend\n",
|
||
"of President Hoover, and the senior apostle of the official economic view, took\n",
|
||
"to the radio to remind the country that President Hoover had said that the\n",
|
||
"\"fundamental business of the country\" was sound. He added firmly, \"The main\n",
|
||
"point which I want to emphasize is the fundamental soundness of [the] great mass\n",
|
||
"of economic activities.\" On Wednesday, Waddill Catchings, of Goldman, Sachs,\n",
|
||
"announced on return from a western trip that general business conditions were\n",
|
||
"\"unquestionably fundamentally sound.\" (The same, by then, could not\n",
|
||
"unquestionably be said of all Goldman, Sachs.) Arthur Brisbane told Hearst\n",
|
||
"readers: \"To comfort yourself, if you lost, think of the people living near\n",
|
||
"Mount Pelee, ordered to abandon their homes.\" Most important, perhaps, from\n",
|
||
"Pocantico Hills came the first public statement by John D. Rockefeller in\n",
|
||
"several decades. So far as the record shows, it was spontaneous. However,\n",
|
||
"someone in Wall Street—perhaps someone who knew that another appeal to President\n",
|
||
"Hoover to say something specifically encouraging about stocks would be\n",
|
||
"useless—may have realized that a statement from Rockefeller would, if anything,\n",
|
||
"be better. The statement ran: \"Believing that fundamental conditions of the\n",
|
||
"country are sound ... my son and I have for some days been purchasing sound\n",
|
||
"common stocks.\" The statement was widely applauded, although Eddie Cantor,\n",
|
||
"describing himself as Comedian, Author, Statistician, and Victim, said later,\n",
|
||
"\"Sure, who else had any money left?\" 4The accepted Wall Street explanation of\n",
|
||
"Wednesday's miracle was not the reassurance but the dividend news of the day\n",
|
||
"before. This also, without much question, was somewhat organized. U.S. Steel had\n",
|
||
"declared an extra dividend; American Can had not only declared an extra but had\n",
|
||
"increased its regular dividend. These errant sunbeams were deeply welcome in the\n",
|
||
"dark canyons of lower Manhattan.Just before the Rockefeller statement arrived,\n",
|
||
"things looked good enough on the Exchange so that Richard Whitney felt safe in\n",
|
||
"announcing that the market would not open until noon the following day\n",
|
||
"(Thursday) and that on Friday and Saturday it would stay shut. The announcement\n",
|
||
"was greeted by cheers. Nerves were clearly past the breaking point. On La Salle\n",
|
||
"Street in Chicago a boy exploded a firecracker. Like wildfire the rumor spread\n",
|
||
"that gangsters whose margin accounts had been closed out were shooting up the\n",
|
||
"street. Several squad cars of police arrived to make them take their losses like\n",
|
||
"honest men. In New York the body of a commission merchant was fished out of the\n",
|
||
"Hudson. The pockets contained $9.40 in change and some margin calls.\n",
|
||
"VIIAt the short session of three hours on Thursday, October 31, well over seven\n",
|
||
"million shares were traded, and the market made another good gain. The Times\n",
|
||
"industrials were up 21 points. The weekly return of the Federal Reserve Bank\n",
|
||
"showed a drop in brokers' loans by more than a billion, the largest weekly drop\n",
|
||
"on record. Margin requirements had already been cut to 25 per cent; now the\n",
|
||
"Federal Reserve Banks lowered the rediscount rate from 6 to 5 per cent. The\n",
|
||
"Reserve Banks also launched vigorous open-market purchases of bonds to ease\n",
|
||
"money rates and liberalize the supply of credit. The boom had collapsed; the\n",
|
||
"restraint that had previously been contemplated could now give way to a policy\n",
|
||
"of active encouragement to the market. On all these happy portents the market\n",
|
||
"closed down for Friday, Saturday, and Sunday. They were not days of rest.\n",
|
||
"Brokerage offices were fully staffed, and the Exchange floor was open for\n",
|
||
"completion of trades and also for straightening out innumerable\n",
|
||
"misunderstandings and mistakes. It was noted that on Friday a visitor to the\n",
|
||
"galleries could not have told the market was suspended.The weekend brought one\n",
|
||
"piece of bad news. That was the announcement on Saturday of the failure of the\n",
|
||
"$20,000,000 Foshay enterprises of Minneapolis. Foshay owned utilities in some\n",
|
||
"twelve states, Canada, Mexico, and Central America, and an assortment of hotels,\n",
|
||
"flour mills, banks, manufacturing and retail establishments wherever he had\n",
|
||
"happened to buy them. The 32-story obelisk, commemorating the enterprise, which\n",
|
||
"still dominates the Minneapolis skyline, had been opened with fitting ceremony\n",
|
||
"by Secretary of War James W. Good, only in August. (Secretary Good had referred\n",
|
||
"to it as the \"Washington Monument of the Northwest.\")5 By all but the most\n",
|
||
"technical of considerations, Foshay was bankrupt at that festive time. His\n",
|
||
"survival depended on his ability to continue merchandising stock to the public.\n",
|
||
"The market crash eliminated this source of revenue and made him dependent on the\n",
|
||
"wholly inadequate earnings of his enterprises. On all other fronts the news was\n",
|
||
"all good. Alfred P. Sloan, Jr., President of the General Motors Corporation,\n",
|
||
"said: \"Business is sound.\" The Ford Motor Company emphasized a similar\n",
|
||
"conviction by announcing a general reduction in its prices: \"...we feel that\n",
|
||
"such a step is the best contribution that could be made to assure a continuation\n",
|
||
"of good business.\" The Roadster was cut from $450 to $435; the Phaeton from $460\n",
|
||
"to $440; the Tudor Sedan from $525 to $500. For the three days that the market\n",
|
||
"was closed the papers carried stories of the accumulation of buying orders and,\n",
|
||
"in some indefinable way, the stories had a greater ring of conviction than the\n",
|
||
"week before. The market, after all, had closed after an excellent two-day rally.\n",
|
||
"As Barron's pointed out, it could now be believed that stocks were selling \"ex-\n",
|
||
"hopes and romance.\" On Monday, the Commercial National Bank and Trust Company\n",
|
||
"took five columns in the Times to advertise \"...our belief and conviction that\n",
|
||
"the general industrial and business condition of the country is fundamentally\n",
|
||
"sound and is essentially unimpaired.\"That day the market started on another\n",
|
||
"ghastly slump. VIIIOver the weekend the financial community had almost\n",
|
||
"certainly been persuaded by its own organized and spontaneous efforts at cheer.\n",
|
||
"The papers described the reaction of professional Wall Street to Monday's market\n",
|
||
"as one of stunned surprise, disbelief, and shock. Volume was smaller than the\n",
|
||
"week before, but still well above six million. The whole list was weak;\n",
|
||
"individual issues made big losses; the Times industrials were down 22 points for\n",
|
||
"the day. Compared with anything but the week before, this was very bad. When\n",
|
||
"measured against the bright hopes for that day, it was most\n",
|
||
"distressing.Explanations varied. The rumor recurred that the \"organized support\"\n",
|
||
"was selling stocks, and Mr. Lamont, on meeting the press, added a minor footnote\n",
|
||
"to this now completed story. He said he didn't know—the organized support was\n",
|
||
"really not that well organized. The most plausible explanation is that everyone\n",
|
||
"was feeling cheerful but the public. As before and later, the weekend had been a\n",
|
||
"time of thought, and out of thought had come pessimism and a decision to sell.\n",
|
||
"So, as on other Mondays, no matter how cheerful the superficial portents, the\n",
|
||
"selling orders poured in in volume.By now it was also evident that the\n",
|
||
"investment trusts, once considered a buttress of the high plateau and a built-in\n",
|
||
"defense against collapse, were really a profound source of weakness. The\n",
|
||
"leverage, of which people only a fortnight before had spoken so knowledgeably\n",
|
||
"and even affectionately, was now fully in reverse. With remarkable celerity it\n",
|
||
"removed all of the value from the common stock of a trust. As before, the case\n",
|
||
"of a typical trust, a small one, is worth contemplating. Let it be supposed that\n",
|
||
"it had securities in the hands of the public which had a market value of\n",
|
||
"$10,000,000 in early October. Of this, half was in common stock, half in bonds\n",
|
||
"and preferred stock. These securities were fully covered by the current market\n",
|
||
"value of the securities owned. In other words, the trust's portfolio contained\n",
|
||
"securities with a market value also of $10,000,000. A representative portfolio\n",
|
||
"of securities owned by such a trust would, in the early days of November, have\n",
|
||
"declined in value by perhaps half. (Values of many of these securities by later\n",
|
||
"standards would still be handsome; on November 4, the low for Tel and Tel was\n",
|
||
"still 233, for General Electric it was 234, and for Steel 183.) The new\n",
|
||
"portfolio value, $5,000,000, would be only enough to cover the prior claim on\n",
|
||
"assets of the bonds and preferred stock. The common stock would have nothing\n",
|
||
"behind it. Apart from expectations, which were by no means bright, it was now\n",
|
||
"worthless.This geometrical ruthlessness was not exceptional. On the contrary, it\n",
|
||
"was everywhere at work on the stock of the leverage trusts. By early November,\n",
|
||
"the stock of most of them had become virtually unsalable. To make matters worse,\n",
|
||
"many of them were traded on the Curb or the out-of-town exchanges where buyers\n",
|
||
"were few and the markets thin.Never was there a time when more people wanted\n",
|
||
"more money more urgently than in those days. The word that a man had \"got\n",
|
||
"caught\" by the market was the signal for his creditors to descend on him like\n",
|
||
"locusts. Many who were having trouble meeting their margin calls wanted to sell\n",
|
||
"some stocks so they could hold the rest and thus salvage something from their\n",
|
||
"misfortunes. But such people now found that their investment trust securities\n",
|
||
"could not be sold for any appreciable sum and perhaps not at all. They were\n",
|
||
"forced, as a result, to realize on their good securities. Standard stocks like\n",
|
||
"Steel, General Motors, Tel and Tel were thus dumped on the market in abnormal\n",
|
||
"volume, with the effect on prices that had already been fully revealed. The\n",
|
||
"great investment trust boom had ended in a unique manifestation of Gresham's Law\n",
|
||
"in which the bad stocks were driving out the good. The stabilizing effects of\n",
|
||
"the huge cash resources of the investment trusts had also proved a mirage. In\n",
|
||
"the early autumn the cash and liquid resources of the investment trusts were\n",
|
||
"large. Many trusts had been attracted by the handsome returns in the call\n",
|
||
"market. (The speculative circle had been closed. People who speculated in the\n",
|
||
"stock of investment trusts were in effect investing in companies which provided\n",
|
||
"the funds to finance their own speculation.) But now, as reverse leverage did\n",
|
||
"its work, investment trust managements were much more concerned over the\n",
|
||
"collapse in the value of their own stock than in the adverse movements in the\n",
|
||
"stock list as a whole. The investment trusts had invested heavily in each other.\n",
|
||
"As a result the fall in Blue Ridge hit Shenandoah, and the resulting collapse in\n",
|
||
"Shenandoah was even more horrible for the Goldman Sachs Trading\n",
|
||
"Corporation.Under these circumstances, many of the trusts used their available\n",
|
||
"cash in a desperate effort to support their own stock. However, there was a vast\n",
|
||
"difference between buying one's stock now when the public wanted to sell and\n",
|
||
"buying during the previous spring—as Goldman Sachs Trading Corporation had\n",
|
||
"done—when the public wanted to buy and the resulting competition had sent prices\n",
|
||
"higher and higher. Now the cash went out and the stock came in, and prices were\n",
|
||
"either not perceptibly affected or not for long. What six months before had been\n",
|
||
"a brilliant financial maneuver was now a form of fiscal self-immolation. In the\n",
|
||
"last analysis, the purchase by a firm of its own stock is the exact opposite of\n",
|
||
"the sale of stocks. It is by the sale of stock that firms ordinarily\n",
|
||
"grow.However, none of this was immediately apparent. If one has been a financial\n",
|
||
"genius, faith in one's genius does not dissolve at once. To the battered but\n",
|
||
"unbowed genius, support of the stock of one's own company still seemed a bold,\n",
|
||
"imaginative, and effective course. Indeed, it seemed the only alternative to\n",
|
||
"slow but certain death. So to the extent that their cash resources allowed, the\n",
|
||
"managements of the trusts chose faster, though equally certain death. They\n",
|
||
"bought their own worthless stock. Men have been swindled by other men on many\n",
|
||
"occasions. The autumn of 1929 was, perhaps, the first occasion when men\n",
|
||
"succeeded on a large scale in swindling themselves.The time has now come to\n",
|
||
"complete the chronicle of the last days of the crisis. IXTuesday,\n",
|
||
"November 5, was election day, and the market was closed all day. In the New York\n",
|
||
"mayoralty race, the Democratic incumbent, James J. Walker, scored a landslide\n",
|
||
"victory over his Republican opponent, F. H. La Guardia, who had been soundly\n",
|
||
"denounced by the Democrats as a socialist. Babson, in a statement, called for\n",
|
||
"poise, discernment, judicious coin-age, and old-fashioned common sense. On\n",
|
||
"Wednesday the market reopened for the first of a new series of short sessions of\n",
|
||
"three hours. These were the compromise on the question of closing which had been\n",
|
||
"reached the previous week. Nearly six million shares were traded in this session\n",
|
||
"or the equivalent of ten million shares on a full day. There was another\n",
|
||
"sickening slide. U.S. Steel opened at 181, and, by what one paper called a\n",
|
||
"succession of \"feverish dips,\" went to 165. Auburn Automobile lost 66 points;\n",
|
||
"Otis Elevator lost 45. The Times industrials were off 37 points for the day, or\n",
|
||
"only 6 points less than on the terrible Tuesday eight days earlier. Where would\n",
|
||
"it all end? There was also disturbing news from beyond the market. Fundamentals\n",
|
||
"seemed to be turning sour. The week's figures on carloadings showed a heavy drop\n",
|
||
"as compared with the year before. The steel rate was significantly down from the\n",
|
||
"preceding week. More serious, the slump had extended to the commodity markets.\n",
|
||
"On previous days these had reacted sympathetically with the stock market. On\n",
|
||
"this Wednesday they had troubles of their own. Cotton was sharply off in the\n",
|
||
"heaviest trading in weeks. References were made to \"panic\" in the wheat market\n",
|
||
"when the price dropped vertically at noon.On Thursday the stock market was\n",
|
||
"steady to higher, but on Friday it took a small drop. People had another weekend\n",
|
||
"of contemplation. This time there was no talk of an accumulation of buying\n",
|
||
"orders; indeed, there was little good news of any kind. On Monday, November 11,\n",
|
||
"came another drastic slump. For the next two days trading was heavy—the Exchange\n",
|
||
"was still on short hours—and prices went down still more. In these three days,\n",
|
||
"November 11, 12, and 13, the Times industrials lost another 50 points.Of all the\n",
|
||
"days of the crash, these without doubt were the dreariest. Organized support had\n",
|
||
"failed. For the moment even organized reassurance had been abandoned. All that\n",
|
||
"could be managed was some sardonic humor. It was noted that the margin calls\n",
|
||
"going out by Western Union that week carried a small sticker: \"Remember them at\n",
|
||
"home with a cheery Thanksgiving telegram, the American way for this American\n",
|
||
"day.\" Clerks in downtown hotels were said to be asking guests whether they\n",
|
||
"wished the room for sleeping or jumping. Two men jumped hand-in-hand from a high\n",
|
||
"window in the Ritz. They had a joint account. The Wall Street Journal, becoming\n",
|
||
"biblical, told its readers: \"Verily, I say, let the fear of the market be the\n",
|
||
"law of thy life, and abide by the words of the bond salesman.\" The financial\n",
|
||
"editor of the Times, who by this time showed signs of being satisfied with the\n",
|
||
"crash and perhaps even of feeling that it had gone too far, said: \"Probably none\n",
|
||
"of the present generation will be able to speak again ... of a 'healthy\n",
|
||
"reaction.' There are many signs that the phrase is entirely out of date.\"\n",
|
||
"CHAPTER VIIAftermath IIN THE WEEK or so following Black Thursday, the London\n",
|
||
"penny press told delightedly of the scenes in downtown New York. Speculators\n",
|
||
"were hurling themselves from windows; pedestrians picked their way delicately\n",
|
||
"between the bodies of fallen financiers. The American correspondent for The\n",
|
||
"Economist wrote an indignant column for his paper protesting against this\n",
|
||
"picture of imaginary carnage.In the United States the suicide wave that followed\n",
|
||
"the stock market crash is also a part of the legend of 1929. In fact, there was\n",
|
||
"none. For several years before 1929, the suicide rate had been gradually rising.\n",
|
||
"It continued to increase in that year, with a further and much sharper increase\n",
|
||
"in 1930, 1931, and 1932—years when there were many things besides the stock\n",
|
||
"market to cause people to conclude that life was no longer worth living. The\n",
|
||
"statistics for New Yorkers, who might be assumed to have had a special\n",
|
||
"propensity for self-destruction, derived from their special propinquity to the\n",
|
||
"market, show only a slight deviation from those for the country as a whole.\n",
|
||
"Since the suicide myth is so well established, it may be useful to give the\n",
|
||
"detailed figures. They are as follows: NUMBER OF SUICIDES PER 100,000 OF\n",
|
||
"POPULATION 1925–34 Year For Registration Area* For New York City 1925 12.1 14.4\n",
|
||
"1926 12.8 13.7 1927 13.3 15.7 1928 13.6 15.7 1929 14.0 17.0 1930 15.7 18.7 1931\n",
|
||
"16.8 19.7 1932 17.4 21.3 1933 15.9 18.5 1934 14.9 17.0 Since the market crash\n",
|
||
"took place late in the year, there could have been a substantial increase in\n",
|
||
"suicides in late October and thereafter which still would not be great enough to\n",
|
||
"affect the figures for the year as a whole. However, figures on the causes of\n",
|
||
"death by months are also available for 1929.1 These show that the number of\n",
|
||
"suicides in October and November was comparatively low—in October, 1331 suicides\n",
|
||
"in all the United States, and in November, 1344. In only three other\n",
|
||
"months—January, February, and September—did fewer people destroy themselves.\n",
|
||
"During the summer months, when the market was doing beautifully, the number of\n",
|
||
"suicides was substantially higher. One can only guess how the suicide myth\n",
|
||
"became established. Like alcoholics and gamblers, broken speculators are\n",
|
||
"supposed to have a propensity for self-destruction. At a time when broken\n",
|
||
"speculators were plentiful, the newspapers and the public may have simply\n",
|
||
"supplied the corollary. Alternatively, suicides that in other times would have\n",
|
||
"evoked the question, \"Why do you suppose he did it?\" now had the motive assigned\n",
|
||
"automatically: \"The poor fellow was caught in the crash.\" Finally, it must be\n",
|
||
"noted that, although suicides did not increase sharply either in the months of\n",
|
||
"the crash or in 1929 as a whole, the rate did rise in the later depression\n",
|
||
"years. In memory some of these tragedies may have been moved back a year or two\n",
|
||
"to the time of the stock market crash.The weight of the evidence suggests that\n",
|
||
"the newspapers and the public merely seized on such suicides as occurred to show\n",
|
||
"that people were reacting appropriately to their misfortune. Enough deaths could\n",
|
||
"be related in one way or another to the market to serve. Beginning soon after\n",
|
||
"Black Thursday, stories of violent self-destruction began to appear in the\n",
|
||
"papers with some regularity. Curiously, though another myth runs strongly to the\n",
|
||
"contrary, few people in these days followed the classical method of jumping from\n",
|
||
"a high window. One would-be suicide jumped into the Schuylkill River, although\n",
|
||
"he changed his mind when he hit the water and was fished out. The head of the\n",
|
||
"Rochester Gas and Electric Company took gas. Another martyr dipped himself in\n",
|
||
"gasoline and touched himself off. He not only made good his escape from his\n",
|
||
"margin calls, but took his wife with him. There was also the suicide of J. J.\n",
|
||
"Riordan.Riordan's death made large headlines in the newspapers on Sunday,\n",
|
||
"November 10. The papers obviously had sensed a story not only in his death\n",
|
||
"itself, but also in the manner of its announcement. Riordan was a widely-known\n",
|
||
"and popular figure among New York Democrats. He had been treasurer of one of\n",
|
||
"Mayor Walker's campaigns and also of one of Al Smith's. He and Smith were close\n",
|
||
"friends and business associates. Al Smith was a member of the board of directors\n",
|
||
"of the recently-organized County Trust Company, of which Riordan was the\n",
|
||
"president. On Friday, November 8, Riordan went to his bank, took a pistol from a\n",
|
||
"teller's cage, went home and shot himself. Al Smith was notified, and his sorrow\n",
|
||
"over the death of his friend was not diminished by the knowledge that the news\n",
|
||
"might start a serious run on their bank. A medical examiner was called but\n",
|
||
"further notification was withheld until the following day (Saturday) at noon,\n",
|
||
"when the bank had closed for the weekend. There had been a long wake through\n",
|
||
"which the distinguished mourners had kept one eye on the corpse and the other on\n",
|
||
"the clock.The medical examiner first implied that he had postponed notification\n",
|
||
"out of a feeling of deep responsibility for depositors of the County Trust. This\n",
|
||
"was a formidable exercise of discretion; carried to its logical conclusion, it\n",
|
||
"meant that all deaths would have to be weighed by the attending doctor for their\n",
|
||
"financial consequences. Later it was tacitly conceded that the decision was Al\n",
|
||
"Smith's. So great was Smith's prestige—and also the general nervousness—that the\n",
|
||
"action was not questioned.For some days rumors had been circulating that Riordan\n",
|
||
"had been wiped out by the crash. Now his friends rallied to his defense, some\n",
|
||
"with vehement assertions that he never played the market. He had been deeply\n",
|
||
"involved, as subsequent Senate committee investigations of the stock market\n",
|
||
"revealed, but a hurried audit of the bank showed that all of its funds were\n",
|
||
"intact. This fact was well-publicized over the weekend. The City Administration\n",
|
||
"boldly announced that it was leaving its deposits with the bank, which was a\n",
|
||
"trifle like saying that it would remain on speaking terms with Tammany Hall.\n",
|
||
"Raskob temporarily assumed the chairmanship. No run developed. The Church\n",
|
||
"concluded that Riordan, a Catholic, had been temporarily deranged and thus was\n",
|
||
"eligible for burial in consecrated ground. Among the honorary pallbearers were\n",
|
||
"Al Smith, Herbert Lehman, and John J. Raskob, and among those attending the\n",
|
||
"funeral were Mayor Frank Hague, Vincent Astor, Grover Whalen, James A. Farley,\n",
|
||
"and M. J. Meehan, the market operator.Two and a half years later, on Saturday,\n",
|
||
"March 12, 1932, Ivar Kreuger shot himself dead in his Paris apartment at eleven\n",
|
||
"o'clock in the morning local time. This was six hours before the New York Stock\n",
|
||
"Exchange closed. With the cooperation of the Paris police, the news was withheld\n",
|
||
"until the market closed. Later a congressional committee was exceedingly\n",
|
||
"critical of this delay, and Al Smith's action was cited in the defense. In the\n",
|
||
"case of Kreuger, it should be added, the security system of the Paris police was\n",
|
||
"less than perfect. It is fairly certain that there was heavy selling that\n",
|
||
"morning—including heavy short selling—of Kreuger and Toll by continental\n",
|
||
"interests.2 IIIn many ways the effect of the crash on embezzlement was\n",
|
||
"more significant than on suicide. To the economist embezzlement is the most\n",
|
||
"interesting of crimes. Alone among the various forms of larceny it has a time\n",
|
||
"parameter. Weeks, months, or years may elapse between the commission of the\n",
|
||
"crime and its discovery. (This is a period, incidentally, when the embezzler has\n",
|
||
"his gain and the man who has been embezzled, oddly enough, feels no loss. There\n",
|
||
"is a net increase in psychic wealth.) At any given time there exists an\n",
|
||
"inventory of undiscovered embezzlement in—or more precisely not in—the country's\n",
|
||
"businesses and banks. This inventory—it should perhaps be called the\n",
|
||
"bezzle—amounts at any moment to many millions of dollars. It also varies in size\n",
|
||
"with the business cycle. In good times people are relaxed, trusting, and money\n",
|
||
"is plentiful. But even though money is plentiful, there are always many people\n",
|
||
"who need more. Under these circumstances the rate of embezzlement grows, the\n",
|
||
"rate of discovery falls off, and the bezzle increases rapidly. In depression all\n",
|
||
"this is reversed. Money is watched with a narrow, suspicious eye. The man who\n",
|
||
"handles it is assumed to be dishonest until he proves himself otherwise. Audits\n",
|
||
"are penetrating and meticulous. Commercial morality is enormously improved. The\n",
|
||
"bezzle shrinks. The stock market boom and the ensuing crash caused a traumatic\n",
|
||
"exaggeration of these normal relationships. To the normal needs for money, for\n",
|
||
"home, family and dissipation, was added, during the boom, the new and\n",
|
||
"overwhelming requirement for funds to play the market or to meet margin calls.\n",
|
||
"Money was exceptionally plentiful. People were also exceptionally trusting. A\n",
|
||
"bank president who was himself trusting Kreuger, Hopson, and Insull was\n",
|
||
"obviously unlikely to suspect his lifelong friend the cashier. In the late\n",
|
||
"twenties the bezzle grew apace.Just as the boom accelerated the rate of growth,\n",
|
||
"so the crash enormously advanced the rate of discovery. Within a few days,\n",
|
||
"something close to universal trust turned into something akin to universal\n",
|
||
"suspicion. Audits were ordered. Strained or preoccupied behavior was noticed.\n",
|
||
"Most important, the collapse in stock values made irredeemable the position of\n",
|
||
"the employee who had embezzled to play the market. He now confessed. After the\n",
|
||
"first week or so of the crash, reports of defaulting employees were a daily\n",
|
||
"occurrence. They were far more common than the suicides. On some days\n",
|
||
"comparatively brief accounts occupied a column or more in the Times. The amounts\n",
|
||
"were large and small, and they were reported from far and wide.The most\n",
|
||
"spectacular embezzlement of the period—the counterpart of the Riordan\n",
|
||
"suicide—was the looting of the Union Industrial Bank of Flint, Michigan. The\n",
|
||
"gross take, estimates of which grew alarmingly as the investigation proceeded,\n",
|
||
"was stated in The Literary Digest later in the year to be $3,592,000.3In the\n",
|
||
"beginning this embezzlement was a matter of individual initiative. Unknown to\n",
|
||
"each other, a number of the bank's officers began making away with funds.\n",
|
||
"Gradually they became aware of each other's activities, and since they could\n",
|
||
"scarcely expose each other, they co-operated. The enterprise eventually embraced\n",
|
||
"about a dozen people, including virtually all of the principal officers of the\n",
|
||
"bank. Operations were so well organized that even the arrival of bank examiners\n",
|
||
"at the local hotels was made known promptly to members of the syndicate.Most of\n",
|
||
"the funds which were purloined had been deposited with the bank to be loaned in\n",
|
||
"the New York call market. The money was duly dispatched to New York but promptly\n",
|
||
"recalled while the records continued to show that it was there. The money was\n",
|
||
"then returned once more to New York and put into stocks. In the spring of 1929\n",
|
||
"the group was about $100,000 ahead. Then, unfortunately, it went short just as\n",
|
||
"the market soared into the blue yonder of the summer sky. This was so costly\n",
|
||
"that the group was induced to return to a long position, which it did just\n",
|
||
"before the crash. The crash, needless to say, was mortal.Each week during the\n",
|
||
"autumn more such unfortunates were revealed in their misery. Most of them were\n",
|
||
"small men who had taken a flier in the market and then become more deeply\n",
|
||
"involved. Later they had more impressive companions. It was the crash, and the\n",
|
||
"subsequent ruthless contraction of values which, in the end, exposed the\n",
|
||
"speculation by Kreuger, Hopson, and Insull with the money of other people.\n",
|
||
"Should the American economy ever achieve permanent full employment and\n",
|
||
"prosperity, firms should look well to their auditors. One of the uses of\n",
|
||
"depression is the exposure of what auditors fail to find. Bagehot once observed:\n",
|
||
"\"Every great crisis reveals the excessive speculations of many houses which no\n",
|
||
"one before suspected.\"4 IIIIn mid-November 1929, at long, long last, the\n",
|
||
"market stopped falling—at least, for a while. The low was on Wednesday, November\n",
|
||
"13. On that day the Times industrials closed at 224 down from 452, or by almost\n",
|
||
"exactly one half since September 3. They were also by then down 82 points—about\n",
|
||
"one quarter—from the close on that day barely two weeks before when John D.\n",
|
||
"Rockefeller had announced that he and his son were buying common stocks. On\n",
|
||
"November 13 there was another Rockefeller story: it was said that the family had\n",
|
||
"entered a million-share buying order to peg Standard Oil of New Jersey at 50.\n",
|
||
"During the rest of November and December the course of the market was moderately\n",
|
||
"up. The decline had run its course. However, the end coincided with one last\n",
|
||
"effort at reassurance. No one can say for sure that it did no good. One part was\n",
|
||
"the announcement by the New York Stock Exchange of an investigation of short\n",
|
||
"selling. Inevitably in the preceding weeks there had been rumors of bear raids\n",
|
||
"on the market and of fortunes being made by the shorts. The benign people known\n",
|
||
"as \"they,\" who once had put the market up, were now a malign influence putting\n",
|
||
"it down and making money out of the common disaster. In the early days of the\n",
|
||
"crash it was widely believed that Jesse L. Livermore, a Bostonian with a large\n",
|
||
"and unquestionably exaggerated reputation for bear operations, was heading a\n",
|
||
"syndicate that was driving the market down. So persistent did these rumors\n",
|
||
"become that Livermore, whom few had thought sensitive to public opinion, issued\n",
|
||
"a formal denial that he was involved in any deflationary plot. \"What little\n",
|
||
"business I have done in the stock market,\" he said, \"has always been as an\n",
|
||
"individual and will continue to be done on such basis.\" As early as October 24,\n",
|
||
"The Wall Street Journal, then somewhat less reserved in its view of the world\n",
|
||
"than now, complained that \"there has been a lot of short selling, a lot of\n",
|
||
"forced selling, and a lot of selling to make the market look bad.\" Such\n",
|
||
"suspicions the Exchange authorities now sought to dispel. Nothing came of the\n",
|
||
"study.A more important effort at reassurance was made by President Hoover.\n",
|
||
"Presumably he was still indifferent to the fate of the stock market. But he\n",
|
||
"could not be indifferent to the much publicized fundamentals, which by now were\n",
|
||
"behaving worse each week. Prices of commodities were falling. Freight-car\n",
|
||
"loadings, pig iron and steel production, coal output, and automobile production\n",
|
||
"were also all going down. So, as a result, was the general index of industrial\n",
|
||
"production. Indeed, it was falling much more rapidly than in the sharp postwar\n",
|
||
"depression of 1920–21. There were alarming stories of the drop in consumer\n",
|
||
"buying, especially of more expensive goods. It was said that sales of radio sets\n",
|
||
"in New York had fallen by half since the crash. Mr. Hoover's first step was out\n",
|
||
"of the later works of John Maynard Keynes. Precisely as Keynes and Keynesians\n",
|
||
"would have advised, he announced a cut in taxes. The rate on both individuals\n",
|
||
"and corporations was cut by one full percentage point. This reduced the income\n",
|
||
"tax of a head of a family with no dependents and an income of $4000 by two-\n",
|
||
"thirds. The man with $5000 got a similar reduction. The tax of a married man\n",
|
||
"with no dependents and an income of $10,000 was cut in half. These were dramatic\n",
|
||
"reductions, but their effect was sadly mitigated by the fact that for most\n",
|
||
"people the taxes being cut were already insignificant. The man with $4000 had\n",
|
||
"his annual tax burden reduced from $5.63 to $1.88. The man with $5000 got a cut\n",
|
||
"from $16.88 to $5.63. For the man with $10,000 the reduction in annual tax was\n",
|
||
"from $120 to $65. The step, nonetheless, was well received as a contribution to\n",
|
||
"increased purchasing power, expanded business investment, and a general revival\n",
|
||
"of confidence.Mr. Hoover also called a series of meetings on the state of the\n",
|
||
"economy. The leading industrialists, the leading railway executives, the heads\n",
|
||
"of the large utilities, the heads of the important construction companies, the\n",
|
||
"union leaders, and the heads of the farm organizations met in turn with the\n",
|
||
"President during the latter part of November. The procedure in the case of each\n",
|
||
"of the meetings was the same. There was a solemn session with the President,\n",
|
||
"those attending had their picture taken with the President, and there was a\n",
|
||
"press interview at which the conferees gave the press their opinion on the\n",
|
||
"business prospect. The latter, without exception, was highly favorable. After\n",
|
||
"the meeting of the industrial leaders on November 21, which was attended by,\n",
|
||
"among others, Henry Ford, Walter Teagle, Owen D. Young, Alfred P. Sloan, Jr.,\n",
|
||
"Pierre du Pont, Walter Gifford, and Andrew Mellon, the expressions of confidence\n",
|
||
"were so robust that Julius Rosenwald, who also attended, said he feared there\n",
|
||
"might soon be a bad labor shortage.The utility, rail, and construction\n",
|
||
"executives were equally hopeful. Even the heads of the farm organizations were\n",
|
||
"less misanthropic than normal for that time. They said afterward that they had\n",
|
||
"told the President that \"the morale of their industry was better than it had\n",
|
||
"been for years.\" 5This was organized reassurance on a really grand scale, and it\n",
|
||
"attracted some of the most enthusiastic comment of the period. A Wall Street\n",
|
||
"financial writer began his story of the sessions: \"'Order up the Moors!' was\n",
|
||
"Marshal Foch's reply at the first battle of the Marne... 'Order up the business\n",
|
||
"reserves,' directed President Hoover as pessimistic reports flowed in from all\n",
|
||
"quarters following the stock market crash.\" The Philadelphia Record was led to\n",
|
||
"describe the President as \"easily the most commanding figure in the modern\n",
|
||
"science of engineering statesmanship.'\" The Boston Globe said that the nation is\n",
|
||
"now aware \"that it has at the White House a man who believes not in the\n",
|
||
"philosophy of drift, but in the dynamics of mastery.\" 6 IVYet to suppose\n",
|
||
"that President Hoover was engaged only in organizing further reassurance is to\n",
|
||
"do him a serious injustice. He was also conducting one of the oldest, most\n",
|
||
"important—and, unhappily, one of the least understood—rites in American life.\n",
|
||
"This is the rite of the meeting which is called not to do business but to do no\n",
|
||
"business. It is a rite which is still much practiced in our time. It is worth\n",
|
||
"examining for a moment. Men meet together for many reasons in the course of\n",
|
||
"business. They need to instruct or persuade each other. They must agree on a\n",
|
||
"course of action. They find thinking in public more productive or less painful\n",
|
||
"than thinking in private. But there are at least as many reasons for meetings to\n",
|
||
"transact no business. Meetings are held because men seek companionship or, at a\n",
|
||
"minimum, wish to escape the tedium of solitary duties. They yearn for the\n",
|
||
"prestige which accrues to the man who presides over meetings, and this leads\n",
|
||
"them to convoke assemblages over which they can preside. Finally, there is the\n",
|
||
"meeting which is called not because there is business to be done, but because it\n",
|
||
"is necessary to create the impression that business is being done. Such meetings\n",
|
||
"are more than a substitute for action. They are widely regarded as action.The\n",
|
||
"fact that no business is transacted at a no-business meeting is normally not a\n",
|
||
"serious cause of embarrassment to those attending. Numerous formulas have been\n",
|
||
"devised to prevent discomfort. Thus scholars, who are great devotees of the no-\n",
|
||
"business meeting, rely heavily on the exchange-of-ideas justification. To them\n",
|
||
"the exchange of ideas is an absolute good. Any meeting at which ideas are\n",
|
||
"exchanged is, therefore, useful. This justification is nearly ironclad. It is\n",
|
||
"very hard to have a meeting of which it can be said that no ideas were\n",
|
||
"exchanged.Salesmen and sales executives, who also are important practitioners of\n",
|
||
"the no-business gathering, commonly have a different justification and one that\n",
|
||
"has strong spiritual overtones. Out of the warmth of comradeship, the interplay\n",
|
||
"of personality, the stimulation of alcohol, and the inspiration of oratory comes\n",
|
||
"an impulsive rededication to the daily task. The meeting pays for itself in a\n",
|
||
"fuller and better life and the sale of more goods in future weeks and months.\n",
|
||
"The no-business meetings of the great business executives depend for their\n",
|
||
"illusion of importance on something quite different. Not the exchange of ideas\n",
|
||
"or the spiritual rewards of comradeship, but a solemn sense of assembled power\n",
|
||
"gives significance to this assemblage. Even though nothing of importance is said\n",
|
||
"or done, men of importance cannot meet without the occasion seeming important.\n",
|
||
"Even the commonplace observation of the head of a large corporation is still the\n",
|
||
"statement of the head of a large corporation. What it lacks in content it gains\n",
|
||
"in power from the assets back of it.The no-business meeting was an almost\n",
|
||
"perfect instrument for the situation in which President Hoover found himself in\n",
|
||
"the autumn of 1929. The modest tax cut apart, the President was clearly averse\n",
|
||
"to any large-scale government action to counter the developing depression. Nor\n",
|
||
"was it very certain, at the time, what could be done. Yet by 1929 popular faith\n",
|
||
"in laissez faire had been greatly weakened. No responsible political leader\n",
|
||
"could safely proclaim a policy of keeping hands off. The no-business meetings at\n",
|
||
"the White House were a practical expression of laissez faire. No positive action\n",
|
||
"resulted. At the same time they gave a sense of truly impressive action. The\n",
|
||
"conventions governing the no-business session insured that there would be no\n",
|
||
"embarrassment arising from the absence of business. Those who attended accepted\n",
|
||
"as a measure of the importance of the meetings the importance of the people\n",
|
||
"attending. The newspapers also cooperated in emphasizing the importance of the\n",
|
||
"sessions. Had they done otherwise they would, of course, have undermined the\n",
|
||
"value of the sessions as news.In recent times the no-business meeting at the\n",
|
||
"White House—attended by governors, industrialists, representatives of business,\n",
|
||
"labor, and agriculture—has become an established institution of government. Some\n",
|
||
"device for simulating action, when action is impossible, is indispensable in a\n",
|
||
"sound and functioning democracy. Mr. Hoover in 1929 was a pioneer in this field\n",
|
||
"of public administration.As the depression deepened, it was said that Mr.\n",
|
||
"Hoover's meetings had been a failure. This, obviously, reflects a very narrow\n",
|
||
"view. VIn January, February, and March of 1930 the stock market showed a\n",
|
||
"substantial recovery. Then in April the recovery lost momentum, and in June\n",
|
||
"there was another large drop. Thereafter, with few exceptions the market dropped\n",
|
||
"week by week, month by month, and year by year through June of 1932. The\n",
|
||
"position when it finally halted made the worst level during the crash seem\n",
|
||
"memorable by contrast. On November 13, 1929, it may be recalled, the Times\n",
|
||
"industrials closed at 224. On July 8, 1932, they were 58. This value was not\n",
|
||
"much more than the net by which they dropped on the single day of October 28,\n",
|
||
"1929. Standard Oil of New Jersey, which the Rockefellers were believed to have\n",
|
||
"pegged at 50 on November 13, 1929, dropped below 20 in April 1932. On July 8 it\n",
|
||
"was 24. U.S. Steel on July 8 reached a low of 22. On September 3, 1929, it had\n",
|
||
"sold as high as 262. General Motors was a bargain at 8 on July 8, down from 73\n",
|
||
"on September 3, 1929. Montgomery Ward was 4, down from 138. Tel and Tel was 72,\n",
|
||
"and on September 3, 1929, it had sold at 304. Anaconda sold at 4 on July 8. The\n",
|
||
"Commercial and Financial Chronicle observed that \"the copper shares are so low\n",
|
||
"that their fluctuations are of little consequence.\"7 However, comparatively\n",
|
||
"speaking, values in these staple stocks had been well maintained. Things were\n",
|
||
"far worse with the investment trusts. Blue Ridge during the week ending July 8,\n",
|
||
"1932, was 63 cents, and Shenandoah was 50 cents. United Founders and American\n",
|
||
"Founders were both around 50 cents as compared with 70 and 117 (dollars,\n",
|
||
"needless to say) on September 3, 1929. The fears of November 1929 that the\n",
|
||
"investment trusts might go to nothing had been largely realized.No one any\n",
|
||
"longer suggested that business was sound, fundamentally or otherwise. During the\n",
|
||
"week of July 8, 1932, Iron Age announced that steel operations had reached 12\n",
|
||
"per cent of capacity. This was thought of its sort to be a record. Pig iron\n",
|
||
"output was the lowest since 1896. A total of 720,278 shares were traded that day\n",
|
||
"on the New York Stock Exchange.Before all this came to pass there had been more,\n",
|
||
"many more, efforts at reassurance. In the weeks of the crash President Hoover\n",
|
||
"had sagely observed: \"My own experience ... has been that words are not of any\n",
|
||
"great importance in times of economic disturbance.\" This impregnable rule he\n",
|
||
"thereafter forgot. In December he told the Congress that the steps he had\n",
|
||
"taken—the White House no-business conferences in particular—had \"re-established\n",
|
||
"confidence.\" In March 1930, following a flood of optimistic forecasts by his\n",
|
||
"subordinates, Mr. Hoover said that the worst effect of the crash upon\n",
|
||
"unemployment would be ended in sixty days. In May Mr. Hoover said he was\n",
|
||
"convinced \"we have now passed the worst and with continued unity of effort shall\n",
|
||
"rapidly recover.\" Toward the end of the month he said that business would be\n",
|
||
"normal by fall.8 What was perhaps the last word on the policy of reassurance was\n",
|
||
"said by Simeon D. Fess, the Chairman of the Republican National\n",
|
||
"Committee: Persons high in Republican circles are beginning to believe that\n",
|
||
"there is some concerted effort on foot to utilize the stock market as a method\n",
|
||
"of discrediting the Administration. Every time an Administration official gives\n",
|
||
"out an optimistic statement about business conditions, the market immediately\n",
|
||
"drops.9 CHAPTER VIIIAftermath IITHE CRASH blighted the fortunes of many\n",
|
||
"hundreds of thousands of Americans. But among people of prominence worse havoc\n",
|
||
"was worked on reputations. In such circles credit for wisdom, foresight, and,\n",
|
||
"unhappily also, for common honesty underwent a convulsive shrinkage.On the\n",
|
||
"whole, those who had proclaimed during the crash that business was\n",
|
||
"\"fundamentally sound\" were not held accountable for their words. The ritualistic\n",
|
||
"nature of their expression was recognized; then as now no one supposed that such\n",
|
||
"spokesmen knew whether business was sound or unsound. One exception was Mr.\n",
|
||
"Hoover. He undoubtedly suffered as the result of his repeated predictions of\n",
|
||
"imminent prosperity. However, Hoover had converted the simple business ritual of\n",
|
||
"reassurance into a major instrument of public policy. It was certain in\n",
|
||
"consequence to be the subject of political comment.The scholarly forecasters\n",
|
||
"were not so fortunate. People on the whole cherished the discovery that they\n",
|
||
"were not omniscient. Mr. Lawrence disappeared from Princeton. Among economists\n",
|
||
"his voice was not heard again.The Harvard Economic Society, it will be recalled,\n",
|
||
"had come up to the summer of the crash with a valuable reputation for pessimism.\n",
|
||
"This position it abandoned during the summer when the stock market kept on\n",
|
||
"rising and business seemed strong. On November 2, after the crash, the Society\n",
|
||
"concluded that \"the present recession, both for stocks and business, is not the\n",
|
||
"precursor of business depression.\" On November 10 it made its notable estimate\n",
|
||
"that \"a serious depression like that of 1920–21 is outside the range of\n",
|
||
"probability.\" It repeated this judgment on November 23 and on December 21 gave\n",
|
||
"its forecast for the new year: \"A depression seems improbable; [we expect]\n",
|
||
"recovery of business next spring, with further improvement in the fall.\" On\n",
|
||
"January 18, 1930, the Society said, \"There are indications that the severest\n",
|
||
"phase of the recession is over\"; on March 1, that \"manufacturing activity is\n",
|
||
"now—to judge from past periods of contraction—definitely on the road to\n",
|
||
"recovery\", on March 22, \"The outlook continues favorable\"; on March 29, that\n",
|
||
"\"the outlook is favorable\"; on April 19, that \"by May or June the spring\n",
|
||
"recovery forecast in our letters of last December and November should be clearly\n",
|
||
"apparent\"; on May 17, that business \"will turn for the better this month or\n",
|
||
"next, recover vigorously in the third quarter and end the year at levels\n",
|
||
"substantially above normal\"; on May 24 it was suggested that conditions\n",
|
||
"\"continue to justify\" the forecasts of May 17; on June 21, that \"despite\n",
|
||
"existing irregularities\" there would soon be improvement; on June 28 it stated\n",
|
||
"that \"irregular and conflicting movements of business should soon give way to\n",
|
||
"sustained recovery\"; on July 19 it pointed out that \"untoward elements have\n",
|
||
"operated to delay recovery but the evidence nonetheless points to substantial\n",
|
||
"improvement\"; and on August 30, 1930, the Society stated that \"the present\n",
|
||
"depression has about spent its force.\" Thereafter the Society became less\n",
|
||
"hopeful. On November 15, 1930, it said: \"We are now near the end of the\n",
|
||
"declining phase of the depression.\" A year later, on October 31, 1931, it said:\n",
|
||
"\"Stabilization at [present] depression levels is clearly possible.\" 1 Even these\n",
|
||
"last forecasts were wildly optimistic. Somewhat later, its reputation for\n",
|
||
"infallibility rather dimmed, the Society was dissolved. Harvard economics\n",
|
||
"professors ceased forecasting the future and again donned their accustomed garb\n",
|
||
"of humility. Professor Irving Fisher tried hard to explain why he had been\n",
|
||
"wrong. Early in November 1929 he suggested that the whole thing had been\n",
|
||
"irrational and hence beyond prediction. In a statement that was not a model of\n",
|
||
"coherence, he said: \"It was the psychology of panic. It was mob psychology, and\n",
|
||
"it was not, primarily, that the price level of the market was unsoundly high ...\n",
|
||
"the fall in the market was very largely due to the psychology by which it went\n",
|
||
"down because it went down.\"2 The explanation attracted little attention except\n",
|
||
"from the editor of The Commercial and Financial Chronicle. The latter observed\n",
|
||
"with succinct brutality: \"The learned professor is wrong as he usually is when\n",
|
||
"he talks about the stock market.\" The \"mob,\" he added, didn't sell. It got sold\n",
|
||
"out.Before the year was over, Professor Fisher tried again in a book, The Stock\n",
|
||
"Market Crash—and After.3 He argued, and rightly for the moment, that stocks were\n",
|
||
"still on a plateau, albeit a somewhat lower one than before, that the crash was\n",
|
||
"a great accident, that the market had gone up \"principally because of sound,\n",
|
||
"justified expectations of earnings.\" He also argued that prohibition was still a\n",
|
||
"strong force for higher business productivity and profits, and concluded that\n",
|
||
"for \"the immediate future, at least, the outlook is bright.\" This book attracted\n",
|
||
"little attention. One trouble with being wrong is that it robs the prophet of\n",
|
||
"his audience when he most needs it to explain why.Out in Ohio, Professor Dice—he\n",
|
||
"of the parasangs—survived honorably to write and teach for a lifetime about\n",
|
||
"finance.This may be the place also to record another happy ending. Goldman,\n",
|
||
"Sachs and Company rescued its firm name from its delinquent offspring and\n",
|
||
"returned to an earlier role of strict rectitude and stern conservatism. It\n",
|
||
"became known for its business in the most austere of securities. IINew\n",
|
||
"York's two greatest banks, the Chase and the National City, suffered severely in\n",
|
||
"the aftermath. They, of course, shared the general obloquy of the New York\n",
|
||
"bankers which resulted from the great hopes and great disappointments of\n",
|
||
"organized support. But it was also the remarkable misfortune of each that it had\n",
|
||
"as its head in those days a market operator in the grand manner.Of the two, the\n",
|
||
"Chase was the more fortunate. Albert H. Wiggin, variously President, Chairman of\n",
|
||
"the board, and Chairman of the governing board of the Chase, was a speculator\n",
|
||
"and operator, but not an articulate one. However, in 1929 and the years\n",
|
||
"preceding he had been engaging in some astonishing enterprises. In 1929 he\n",
|
||
"received $275,000 compensation as head of the Chase. He was also—or while head\n",
|
||
"of the Chase had been—director of some fifty-nine utility, industrial,\n",
|
||
"insurance, and other corporations and from some of these had also received a\n",
|
||
"handsome salary. Armour and Company had paid him $40,000 to be a member of its\n",
|
||
"finance committee; he got $20,000 a year from the Brooklyn-Manhattan Transit\n",
|
||
"Corporation; at least seven other firms paid him from two to five thousand\n",
|
||
"annually.4 Wisdom and esteem, or even affection, were not the only factors in\n",
|
||
"this compensation. Those who paid were usually clients and prospective borrowers\n",
|
||
"from the Chase. But the most remarkable of Mr. Wiggin's extracurricular\n",
|
||
"interests was a bevy of private companies. Three were personal holding\n",
|
||
"companies, two of which were named sentimentally for his daughters. Three others\n",
|
||
"were incorporated in Canada for highly unsentimental reasons of taxation and\n",
|
||
"corporate reticence.5 These companies were the instruments for an astonishing\n",
|
||
"variety of stock market operations. In one operation in the spring of 1929\n",
|
||
"Shermar Corporation—one of the namesake companies—participated with Harry F.\n",
|
||
"Sinclair and Arthur W. Cutten in a mammoth pool in the common stock of Sinclair\n",
|
||
"Consolidated Oil Company. Even in those tolerant days Sinclair and Cutten were\n",
|
||
"considered rather garish companions for a prominent banker. However, the\n",
|
||
"operation netted Shermar $891,600.37 on no apparent investment.6However, the\n",
|
||
"most breathtaking of Mr. Wiggin's operations were in the stock of the Chase\n",
|
||
"National Bank. These, in turn, were financed by the Chase bank itself. In one\n",
|
||
"exceptionally well-timed coup Shermar Corporation, between September 23 and\n",
|
||
"November 4, 1929, sold short 42,506 shares of Chase stock. (For those to whom\n",
|
||
"short selling is an unrevealed mystery, this meant in effect that it negotiated\n",
|
||
"a loan of 42,506 shares and then sold them at the exceedingly good prices then\n",
|
||
"obtaining. It did this with the intention of buying the same number of shares\n",
|
||
"later at a lower price in order to repay in kind the lender who had provided the\n",
|
||
"original stock. The profit from repaying shares bought at a lower price—always\n",
|
||
"assuming that the price went down—would obviously accrue to Shermar.) Prices did\n",
|
||
"go down superbly; the short sale anticipated perfectly the crash. Then on\n",
|
||
"December 11, 1929, Murlyn Corporation—this was for another daughter—bought\n",
|
||
"42,506 shares of stock from an affiliate of the Chase National Bank and financed\n",
|
||
"this purchase with a loan of $6,588,430 from the Chase National Bank and from\n",
|
||
"the Shermar Corporation. These shares were used to cover Shermar's short sale,\n",
|
||
"i.e., to repay the loan of securities. The profits on the operation—at a time\n",
|
||
"when many other people were doing much, much less well—was $4,008,538.7 People\n",
|
||
"of carping tendencies might hold the profit was earned by the bank, whose stock\n",
|
||
"it was, whose officer Wiggin was, and which had provided the money for the\n",
|
||
"operation. In fact, the gain all went to Wiggin. Mr. Wiggin subsequently\n",
|
||
"defended loans by banks to their own officers to allow them to speculate in\n",
|
||
"their own stock on the grounds that it developed an interest in their\n",
|
||
"institution. However, by this fine of reasoning, loans to finance short sales\n",
|
||
"present a difficulty: presumably they develop an interest in having the\n",
|
||
"institution, and hence its stock, behave as badly as possible. Pressed on this\n",
|
||
"point, Mr. Wiggin expressed doubt as to whether officers should sell their own\n",
|
||
"companies short. At the end of 1932 Mr. Wiggin requested that he not be re-\n",
|
||
"elected Chairman of the Governing Board of the bank. He was approaching sixty-\n",
|
||
"five, and he noted with some slight overstatement that his \"heart and energies\n",
|
||
"[had] been concentrated for many years in promoting the growth, welfare, and\n",
|
||
"usefulness of the Chase National Bank.\"8 It also seems probable that Winthrop W.\n",
|
||
"Aldrich, who had come into the Chase as the result of a merger with the\n",
|
||
"Equitable Trust Company and who represented a more austere tradition in\n",
|
||
"commercial banking—the Equitable was controlled by the Rockefellers—had come to\n",
|
||
"regard Mr. Wiggin as dispensable.9 The Executive Committee of the Chase, \"in\n",
|
||
"order to discharge in some measure the obligations of this bank to Mr.\n",
|
||
"Wiggin,\"10 by unanimous action voted him a life salary of $100,000. It was later\n",
|
||
"brought out that this gesture of inspired generosity had been the impulse of Mr.\n",
|
||
"Wiggin himself. In the months following Mr. Wiggin's retirement his activities\n",
|
||
"became a matter of detailed study by a Senate committee. Mr. Aldrich, his\n",
|
||
"successor, confessed his surprise at the extent and diversity of his\n",
|
||
"predecessor's enterprises and said that the voting of the life salary was a\n",
|
||
"terrible mistake. Mr. Wiggin later renounced the compensation. IIIBy\n",
|
||
"comparison with the National City the troubles of the Chase were slight. Mr.\n",
|
||
"Wiggin was a reserved, some described him as a rather scholarly, man. The head\n",
|
||
"of the National City, Charles E. Mitchell, on the other hand, was a genial\n",
|
||
"extrovert with a talent for headlines. He was known to one and all as a leading\n",
|
||
"prophet of the New Era.In the autumn of 1929 there were rumors in Wall Street\n",
|
||
"that Mitchell would resign. He did not, and the rumors were described by Percy\n",
|
||
"A. Rockefeller, an associate in numerous rather fervent stock market operations\n",
|
||
"and a director of the bank, as \"too absurd to be considered by any sensible\n",
|
||
"person.\"11 For the next two or three years Mitchell was rather out of the news.\n",
|
||
"Then at nine o'clock on the evening of March 21, 1933, he was arrested by\n",
|
||
"Assistant U.S. District Attorney Thomas E. Dewey and charged with evasion of\n",
|
||
"income taxes. Many of the facts were never seriously in dispute. Like Wiggin,\n",
|
||
"Mitchell had been operating extensively in the stock of his own bank, although\n",
|
||
"possibly for more defensible reasons. Nineteen-twenty-nine was a year of bank\n",
|
||
"mergers, and Charles E. Mitchell was no man to resist a trend. By early autumn\n",
|
||
"of 1929 he had all but completed a merger with the Com Exchange Bank. The\n",
|
||
"directors of the two institutions had approved; all that remained was the\n",
|
||
"formality of ratification by the stockholders. Holders of Com Exchange stock\n",
|
||
"were to receive, at their option, four-fifths of a share of National City stock\n",
|
||
"or $360 in cash. The price of National City stock was then above 500, so it was\n",
|
||
"certain that the Com Exchange stockholders would take the stock.Then came the\n",
|
||
"crash. The price of National City stock dropped to around 425, and at any price\n",
|
||
"below 450—four-fifths of which equalled the $360 in cash—the stockholders of the\n",
|
||
"Com Exchange would take money. To buy out all the Com Exchange stockholders for\n",
|
||
"cash would cost the National City around $200 million. That was too much, so\n",
|
||
"Mitchell undertook to save the deal. He began buying National City stock, and\n",
|
||
"during the week of October 28 he arranged to borrow twelve million dollars from\n",
|
||
"J. P. Morgan and Company with which to buy more. (Twelve million was a sizable\n",
|
||
"sum both for Mitchell and for Morgan's, even at that time. Only ten million was\n",
|
||
"actually used, and of this four million was repaid within a week or so. Possibly\n",
|
||
"some of the Morgan partners had second thoughts on the wisdom of the loan.)The\n",
|
||
"coup failed. Like so many others, Mitchell learned how different it was to\n",
|
||
"support a stock when everyone wanted to sell as compared with those days but a\n",
|
||
"few weeks back when everyone wanted to buy. The price of National City stock\n",
|
||
"sank lower and lower. Mitchell reached the end of his resources and gave up.\n",
|
||
"This was no time for false pride, and with some mild prodding from the\n",
|
||
"management, the National City stockholders repudiated that management and\n",
|
||
"rejected the now disastrous deal. Mitchell, however, was left with a formidable\n",
|
||
"debt to J. P. Morgan and Company. This debt was secured by the stock that had\n",
|
||
"been purchased to support the market and by Mitchell's personal holdings, but\n",
|
||
"its value was shrinking grievously. By the end of the year National City stock\n",
|
||
"was near 200, down from more than 500, and close to the value at which Morgan's\n",
|
||
"had accepted it as collateral. Now Mitchell faced another misfortune, or,\n",
|
||
"rather, an earlier piece of good fortune now became a disaster. As an executive\n",
|
||
"of the National City Bank, Mitchell's pay was a modest $25,000. However, the\n",
|
||
"bank had an incentive system which may still hold some sort of record for\n",
|
||
"munificence. After a deduction of 8 per cent, 20 per cent of the profits of the\n",
|
||
"bank and of its security affiliate, the National City Company, were paid into a\n",
|
||
"management fund. This was divided twice a year between the principal officers by\n",
|
||
"an arrangement which must have made for an interesting half hour. Each officer\n",
|
||
"first dropped in a hat an unsigned ballot suggesting the share of the fund that\n",
|
||
"Chairman Mitchell should have. Then each signed a ballot giving his estimate of\n",
|
||
"the worth of each of the other eligible officers, himself excluded. The average\n",
|
||
"of these estimates guided the Executive Committee of the bank in fixing the\n",
|
||
"percentages of the fund each officer was to have.The years 1928 and 1929 were a\n",
|
||
"time of excellent profits. Mitchell's subordinates had also taken a favorable\n",
|
||
"view of his work. For the full year 1928 his cut was $1,316,634.14. 1929 was\n",
|
||
"even better. The division at the end of the first half of that year brought him\n",
|
||
"no less than $1,108,000.12 Dividends and numerous other activities had further\n",
|
||
"augmented his income, and all of this meant a serious tax liability. It would\n",
|
||
"have been easy to sell some National City stock and establish a tax loss, but,\n",
|
||
"as noted, the stock was pledged with J. P. Morgan and Company. Nevertheless,\n",
|
||
"Mitchell sold the stock—to his wife: 18,300 shares were disposed of to this\n",
|
||
"possibly unsuspecting lady at 212, for the exceedingly satisfying loss of\n",
|
||
"$2,872,305.50. This wiped out all tax liability for 1929. Morgan's was not, it\n",
|
||
"appears, notified of the change of ownership of the stock they held. Somewhat\n",
|
||
"later Mitchell reacquired the stock from his wife, also at a price of 212.\n",
|
||
"Before then there had been a further sickening slide in the price, and had\n",
|
||
"Chairman Mitchell bought the stock in the open market rather than from his wife,\n",
|
||
"he could have got it for around 40. Asked about the transaction by Senator\n",
|
||
"Brookhart of Iowa during a Senate hearing, Mitchell, in a burst of candor that\n",
|
||
"must have devastated his lawyer, said: \"I sold this stock, frankly, for tax\n",
|
||
"purposes.\"13 This frankness led directly to his indictment a few weeks\n",
|
||
"later.Following his testimony, Mitchell had resigned from the National City\n",
|
||
"Bank. His trial in New York during May and June of 1933 was something of a\n",
|
||
"sensation, although the headlines were necessarily subordinate to the larger\n",
|
||
"ones currently being made in Washington. In his inaugural address on March 4,\n",
|
||
"Roosevelt had promised to drive the money-changers from the temple. Mitchell was\n",
|
||
"widely regarded as the first.On June 22, Mitchell was acquitted by the jury on\n",
|
||
"all counts. The sales as required by the tax laws were held to be bona fide\n",
|
||
"transactions made in good faith. The Times reporter covering the trial thought\n",
|
||
"that both Mitchell and his lawyer received the verdict with surprise. Attorney\n",
|
||
"General Cummings said that he still believed in the jury system. Mitchell later\n",
|
||
"resumed his career in Wall Street as head of Blyth and Company. The government\n",
|
||
"entered a civil claim for the taxes and won a judgment of $1,100,000 in taxes\n",
|
||
"and penalties. Mitchell appealed the case through to the Supreme Court, lost,\n",
|
||
"and made a final settlement with the government on December 27, 1938. On his\n",
|
||
"behalf it must be stressed that the device by which he sought to reduce his tax\n",
|
||
"liability was far more common then than now. The Senate investigations of 1933\n",
|
||
"and 1934 showed that tax avoidance had brought individuals of the highest\n",
|
||
"respectability into extraordinary financial intercourse with their wives.14\n",
|
||
"IVOur political tradition sets great store by the generalized symbol of evil.\n",
|
||
"This is the wrongdoer whose wrongdoing will be taken by the public to be the\n",
|
||
"secret propensity of a whole community or class. We search avidly for such\n",
|
||
"people, not so much because we wish to see them exposed and punished as\n",
|
||
"individuals, but because we cherish the resulting political discomfort of their\n",
|
||
"friends. To uncover an evil man among the friends of one's foes had long been a\n",
|
||
"recognized method of advancing one's political fortunes. However, in recent\n",
|
||
"times the technique has been greatly improved and refined by the added firmness\n",
|
||
"with which the evil of the evildoer is now attributed to friends, acquaintances,\n",
|
||
"and all who share his way of life. In the nineteen-thirties Wall Street was\n",
|
||
"exceptionally well endowed with enemies. There were some socialists and\n",
|
||
"communists who believed that capitalism should be abolished and obviously did\n",
|
||
"not seek to have its citadel preserved. There were some people who merely\n",
|
||
"thought that Wall Street was bad. There were yet others who did not seek to have\n",
|
||
"Wall Street abolished or who did not care much about its allegedly evil ways but\n",
|
||
"who enjoyed as a matter of course the discomfiture of the rich and the powerful\n",
|
||
"and the proud. There were those who had lost money in Wall Street. Most of all\n",
|
||
"there was the New Deal. The administrations of Coolidge and Hoover had had an\n",
|
||
"extremely overt alliance with the great financial interests which Wall Street\n",
|
||
"symbolized. With the advent of the New Deal the sins of Wall Street became the\n",
|
||
"sins of the political enemy. What was bad for Wall Street was bad for the\n",
|
||
"Republican Party.For anyone who was in search of symbolic evil in Wall Street—of\n",
|
||
"individuals whose misbehavior would stigmatize the whole community—the discovery\n",
|
||
"that the heads of the National City and Chase had been guilty of grave lapses\n",
|
||
"would seem to be almost ideal. These were the two best-known and most\n",
|
||
"influential banks; what could have been better than default here?That the\n",
|
||
"shortcomings of Mr. Wiggin and Mr. Mitchell were much welcomed is, of course,\n",
|
||
"clear. Yet in some indefinable sense they were not of that part of Wall Street\n",
|
||
"that people suspected most. Wall Street's crime, in the eyes of its classical\n",
|
||
"enemies, was less its power than its morals. And the center of immorality was\n",
|
||
"not the banks but the stock market. It was on the stock market that men gambled\n",
|
||
"not alone with their own money, but with the wealth of the country. The stock\n",
|
||
"market, with its promise of easy riches, was what led good if not very wise men\n",
|
||
"to perdition—like the cashier of the local bank who was also a vestryman. The\n",
|
||
"senseless gyrations of the stock market affected farm prices and land values and\n",
|
||
"the renewal of notes and mortgages. Though to the sophisticated radical the\n",
|
||
"banks might be the real menace, sound populist attitudes pointed the finger of\n",
|
||
"suspicion at the New York Stock Exchange. There, accordingly, was the place, if\n",
|
||
"possible, to find the symbol of evil, for there was the institution about which\n",
|
||
"people were ready to believe the worst. The search for a really adequate\n",
|
||
"miscreant in the Stock Exchange began in April of 1932. The task was undertaken\n",
|
||
"by the Senate Committee on Banking and Currency (later by a subcommittee) and\n",
|
||
"its instructions, graced by the usual split infinitive, were \"to thoroughly\n",
|
||
"investigate practices of stock exchanges...\" Under the later guidance of\n",
|
||
"Ferdinand Pecora, this committee became the scourge of commercial, investment,\n",
|
||
"and private bankers. But this was not foreseen when it was organized. The\n",
|
||
"original and more or less exclusive object of the inquiry was the market for\n",
|
||
"securities.On the whole, this part of the investigation was unproductive. The\n",
|
||
"first witness, when the hearings opened on April 11, 1932, was Richard\n",
|
||
"Whitney.15 On November 30, 1929, the Governing Committee of the New York Stock\n",
|
||
"Exchange had passed a resolution of appreciation for the \"efficient and\n",
|
||
"conscientious\" labors of their acting president during the recent storm. It is\n",
|
||
"an \"old saying,\" the Resolution had stated, \"that great emergencies produce the\n",
|
||
"men who are competent to deal with them...\" This sense of indebtedness made it\n",
|
||
"inevitable that when Edward H. H. Simmons retired as President of the Exchange\n",
|
||
"in 1930 after six years in office, Whitney would be elected to succeed him. As\n",
|
||
"President of the Exchange it thus fell to Whitney, in the spring of 1932, to\n",
|
||
"assume the task of protecting the stock market from its critics. Whitney was not\n",
|
||
"in all respects an ingratiating witness. One of his successors in office not\n",
|
||
"long ago compared his general manner and bearing with that of Secretary of\n",
|
||
"Defense Charles E. Wilson at the hearings on his confirmation as Secretary of\n",
|
||
"Defense in early 1953. Whitney admitted to no serious fault in the past\n",
|
||
"operations of the Exchange or even to the possibility of error. He supplied the\n",
|
||
"information that was requested, but he was not unduly helpful to senators who\n",
|
||
"sought to penetrate the mysteries of short selling, sales against the box,\n",
|
||
"options, pools, and syndicates. He seemed to feel that these things were beyond\n",
|
||
"the senators' intelligence. Alternatively he implied that they were things that\n",
|
||
"every intelligent schoolboy understood and it was painful for him to have to go\n",
|
||
"over the obvious. He was so unwise as to get into a discussion of personal\n",
|
||
"economic philosophy with Senator Smith W. Brookhart of Iowa, one of the\n",
|
||
"committee members who believed, devoutly, that the Exchange was the particular\n",
|
||
"invention of the devil. The government, not Wall Street, was responsible for the\n",
|
||
"current bad times, Whitney averred, and the government, he believed, could make\n",
|
||
"its greatest contribution to recovery by balancing the budget and thus restoring\n",
|
||
"confidence. To balance the budget he recommended cutting the pensions and\n",
|
||
"benefits of veterans who had no service-connected disability and also all\n",
|
||
"government salaries. When asked about cutting his own pay he said no—it was\n",
|
||
"\"very little.\" Pressed for the amount, he said that currently it was only about\n",
|
||
"$60,000. His attention was drawn by the committee members to the fact that this\n",
|
||
"was six times what a senator received, but Whitney remained adamantly in favor\n",
|
||
"of cutting the public pay, including that of senators.16 In spite of Whitney's\n",
|
||
"manner, or possibly because of it, several days of questioning produced little\n",
|
||
"evidence of wrongdoing and no identification of wrongdoers. Prior to the crash\n",
|
||
"Whitney had heard generally of syndicates and pools, but he could give no\n",
|
||
"details. He repeatedly assured the committee that the Exchange had these and\n",
|
||
"other matters well under control. He took exception to Senator Brookhart's\n",
|
||
"contention that the market was a gambling hell and should be padlocked. In the\n",
|
||
"end Whitney was excused before he had quite completed his testimony.When the\n",
|
||
"interrogation of Whitney showed clear signs of being unproductive, the committee\n",
|
||
"turned to the famous market operators. These, too, were disappointing. All that\n",
|
||
"could be proved was what everyone knew, namely, that Bernard E. (\"Sell 'em Ben\")\n",
|
||
"Smith, M. J. Meehan, Arthur W. Cutten, Harry F. Sinclair, Percy A. Rockefeller,\n",
|
||
"and others had been engaged in large-scale efforts to rig the market. Harry F.\n",
|
||
"Sinclair, for example, was shown to have engaged in especially extensive\n",
|
||
"operations in Sinclair Consolidated Oil. This was much like identifying William\n",
|
||
"Z. Foster with the Communist Party. It was impossible to imagine Harry Sinclair\n",
|
||
"not being involved in some intricate maneuver in high finance. Moreover,\n",
|
||
"reprehensible as these activities were, it remained that only three short years\n",
|
||
"before they had been regarded with breathless admiration. The problem here was\n",
|
||
"somewhat similar to that encountered in the great red-hunt of the latter\n",
|
||
"forties. Then there was constant embarrassment over the short time that had\n",
|
||
"elapsed since Red Russia had been our gallant Soviet ally. It is true that the\n",
|
||
"big operators, as they appeared on the stand, were not an especially\n",
|
||
"prepossessing group. As noted earlier, Arthur Cutten's memory was extremely\n",
|
||
"defective. M. J. Meehan was in bad health and mistakenly went abroad when he was\n",
|
||
"supposed to go to Washington. (He later apologized handsomely for the error.)\n",
|
||
"Few of the others could remember much about their operations, Napoleonic though\n",
|
||
"they had once seemed. But men cannot be brought to trial for being\n",
|
||
"unprepossessing. And the dubious demeanor and bad memories of the market\n",
|
||
"operators did not directly involve the reputation of the New York Stock\n",
|
||
"Exchange. It is possible to have a poor view of touts, tipsters, and bookies\n",
|
||
"without thinking the worse of Churchill Downs.In earlier times of trouble on the\n",
|
||
"stock market, stock exchange firms had failed, on occasion by the score. In the\n",
|
||
"fall of 1929 the failures were unimportant. In the first week of the crash no\n",
|
||
"member firm of the New York Stock Exchange had to suspend; only one smallish\n",
|
||
"member firm went under during the period of the panic. There were some\n",
|
||
"complaints by customers of mistreatment. But there were more customers who,\n",
|
||
"during the worst days, were carried by their brokers after their margins had\n",
|
||
"been impaired or depleted. The standards of commercial morality of the members\n",
|
||
"of the Exchange would seem to have been well up to the average of the late\n",
|
||
"twenties. They may have been much more rigorous. This would seem to be the most\n",
|
||
"obvious explanation why the Exchange and its members survived so well the\n",
|
||
"investigations of the thirties. They did not come through unscathed, but they\n",
|
||
"suffered no obloquy comparable, say, with that of the great bankers. In the\n",
|
||
"congressional investigations no flagrant miscreant of any kind was uncovered on\n",
|
||
"the Exchange to serve as the symbolic bad apple. Then, on March 10, 1938,\n",
|
||
"District Attorney Thomas E. Dewey—who had arrested Charles E. Mitchell and who\n",
|
||
"somehow has escaped a reputation as the nemesis of Wall Street—ordered the\n",
|
||
"arraignment of Richard Whitney. The charge was grand larceny. VThe rush\n",
|
||
"to get in on the act, to use the recent idiom, when Whitney was arrested is a\n",
|
||
"measure of the yearning for a malefactor in the stock market. It can be compared\n",
|
||
"only with the stampede which followed the announcement by Attorney General\n",
|
||
"Herbert Brownell in the autumn of 1953 that former President Truman had shielded\n",
|
||
"treason. On the day following his first arrest, Whitney was arrested again by\n",
|
||
"New York State Attorney General John J. Bennett. Mr. Bennett had been conducting\n",
|
||
"an investigation of Whitney's affairs, and he bitterly accused Mr. Dewey of\n",
|
||
"legal claim-jumping. In the next few weeks virtually every public body or\n",
|
||
"tribunal with a plausible excuse for doing so, called Whitney to enlarge on his\n",
|
||
"wrongdoing.The detailed story of Richard Whitney's misfortunes do not belong to\n",
|
||
"this chronicle. Many of them occurred after the period with which this history\n",
|
||
"is concerned. There is need here to cover only those operations that were deemed\n",
|
||
"to implicate the market.Whitney's dishonesty was of a casual, rather juvenile\n",
|
||
"sort. Associates of the day have since explained it as the result of an\n",
|
||
"unfortunate failure to realize that the rules, which were meant for other\n",
|
||
"people, also applied to him. Much more striking than Whitney's dishonesty was\n",
|
||
"the clear fact that he was one of the most disastrous businessmen in modern\n",
|
||
"history. Theft was almost a minor incident pertaining to his business\n",
|
||
"misfortunes. In the twenties the Wall Street firm of Richard Whitney and Company\n",
|
||
"was an unspectacular bond house with a modest business. Whitney apparently felt\n",
|
||
"that it provided insufficient scope for his imagination, and with the passing\n",
|
||
"years he moved on to other enterprises, including the mining of mineral colloids\n",
|
||
"and the marketing of peat humus in Florida. He had also become interested in the\n",
|
||
"distilling of alcoholic beverages, mainly applejack, in New Jersey. Nothing is\n",
|
||
"so voracious as a losing business, and eventually Whitney had three of them. To\n",
|
||
"keep them going he borrowed from banks, investment bankers, other stock exchange\n",
|
||
"members, and heavily from his brother, George Whitney, a partner of J. P. Morgan\n",
|
||
"and Company. The loans so negotiated, from the early twenties on, totaled in the\n",
|
||
"millions, many of them unsecured. As time passed Whitney was increasingly\n",
|
||
"pressed. When one loan became due he was forced to replace it with another and\n",
|
||
"to borrow still more for the interest on those outstanding. Beginning in 1933\n",
|
||
"his stock exchange firm was insolvent, although this did not become evident for\n",
|
||
"some five years.17Finally, like so many others, Richard Whitney learned the cost\n",
|
||
"of supporting a stock on a falling market. In 1933, Richard Whitney and\n",
|
||
"Company—the affairs of Whitney and his company were almost completely\n",
|
||
"indistinguishable—had invested in between ten and fifteen thousand shares of\n",
|
||
"Distilled Liquors Corporation, the New Jersey manufacturer of applejack and\n",
|
||
"other intoxicants. The price was $15 a share. In the spring of 1934 the stock\n",
|
||
"reached 45 in over-the-counter trading. In January 1935 it was fisted on the New\n",
|
||
"York Curb Exchange. Inevitably Whitney posted the stock as collateral for\n",
|
||
"various of his loans. Unhappily, popular enthusiasm for the products of the\n",
|
||
"firm, even in the undiscriminating days following repeal, was remarkably slight.\n",
|
||
"The firm made no money and by June 1936 the price of the stock was down to 11.\n",
|
||
"This drop had a disastrous effect on its value as collateral, and the unhappy\n",
|
||
"Whitney tried to maintain the value by buying more of it. (He later made the\n",
|
||
"claim that he wanted to provide the other investors in the company with a market\n",
|
||
"for their stock,18 which if true meant that he was engaging in one of the most\n",
|
||
"selfless acts since the death of Sydney Carton.) All the other investors\n",
|
||
"unloaded on Whitney. At the time of his failure, of the 148,750 shares\n",
|
||
"outstanding, Whitney or his firm owned 137,672. By then the value had dropped to\n",
|
||
"between three and four dollars a share. Mention has been made of the tendency of\n",
|
||
"people in this period to swindle themselves. Whitney, in his effort to support\n",
|
||
"the stock of Distilled Liquors Corporation, unquestionably emerged as the Ponzi\n",
|
||
"of financial self-deception. As the result of his operation he had all his old\n",
|
||
"debts, many new ones incurred in supporting the stock, all the stock—and the\n",
|
||
"stock was nearly worthless.As his position became more complex, Richard Whitney\n",
|
||
"resorted increasingly to an expedient which he had been using for several\n",
|
||
"years—that of posting securities belonging to other people which were in his\n",
|
||
"custody as collateral for his loans. By early 1938 he had reached the end of a\n",
|
||
"surprising capacity to borrow money. Late in the preceding autumn he had had a\n",
|
||
"large last loan from his brother to release securities belonging to the gratuity\n",
|
||
"fund of the Stock Exchange—a fund out of which payments were made on the death\n",
|
||
"of members—which he had appropriated and pledged for a bank loan. He was now\n",
|
||
"desperately, almost pathetically, visiting the most casual of acquaintances in\n",
|
||
"search of funds. The rumor spread that he was in poor condition. Still, on March\n",
|
||
"8, there was a stunned silence on the floor of the Exchange when President\n",
|
||
"Charles R. Gay announced from the rostrum the suspension of Richard Whitney and\n",
|
||
"Company for insolvency. Members were rather more aghast when they learned that\n",
|
||
"Whitney had been engaged in theft on a large scale for a long period.With no\n",
|
||
"small dignity Whitney made a full disclosure of his operations, refused to enter\n",
|
||
"any sort of plea in his own defense, and passed permanently from sight.\n",
|
||
"VIThe failure of the smallest country banker caused more personal hardship,\n",
|
||
"anguish, and privation than the insolvency of Richard Whitney. His victims were\n",
|
||
"almost uniquely able to afford their loss. And the sums which he stole, while\n",
|
||
"substantial, did not place him in the ranks of the great defaulters of the\n",
|
||
"day—they would not have paid the interest on Ivar Kreuger's larceny for a year.\n",
|
||
"Yet, from the point of view of the antagonists of Wall Street, his default was\n",
|
||
"ideal. Rarely has a crime been more joyously received.Whitney's identification\n",
|
||
"was wholly with the Stock Exchange, the symbolic center of sin. Moreover, he had\n",
|
||
"been its president and its uncompromising defender before the Congress and the\n",
|
||
"public in its period of trial. He was a Republican, an arch-conservative, and\n",
|
||
"was loosely associated in the financial community with J. P. Morgan and Company.\n",
|
||
"He had himself taken a strong position in favor of honesty. Speaking in St.\n",
|
||
"Louis in 1932, at a time when his own larceny was already well advanced, Whitney\n",
|
||
"had said sternly that one of the prime necessities \"of a great market is that\n",
|
||
"brokers must be honest and financially responsible.\" He looked forward to the\n",
|
||
"day when the financial supervision by the Exchange of its members would be so\n",
|
||
"strict that failure \"will be next to impossible.\"19 Finally, even by his\n",
|
||
"colleagues, Whitney was regarded as a trifle upstage. In his last days he had\n",
|
||
"been reduced to the ultimate indignity of trying to borrow money from the market\n",
|
||
"operator, Bernard E. Smith. Smith, at best a lower middle-brow figure, later\n",
|
||
"told a Securities and Exchange Commission examiner: \"He came up to see me and\n",
|
||
"said he would like to get this over quickly, and told me he would like to borrow\n",
|
||
"$250,000 on his face. I remarked he was putting a pretty high value on his face,\n",
|
||
"so he said ... his back was to the wall and he had to have $250,000. I told him\n",
|
||
"he had a lot of nerve to ask me for $250,000 when he didn't even bid me the time\n",
|
||
"of day. I told him I frankly didn't like him—that I wouldn't loan him a dime.\"20\n",
|
||
"On any free vote for the man best qualified to bring discredit to Wall Street,\n",
|
||
"Whitney would have won by a wide margin.The parallel between Whitney and a more\n",
|
||
"recent culprit is interesting. During the thirties the New Dealers were\n",
|
||
"exuberantly uncovering the financial derelictions of their opposition. (It is\n",
|
||
"interesting that dishonesty and not the more orthodox offenses of capitalism\n",
|
||
"like abuse of power or the exploitation of the people were in these days the\n",
|
||
"nemesis of conservatives.) In the nineteen-forties and fifties Republicans, as\n",
|
||
"avidly, were discovering that there were New Dealers who had been communists.\n",
|
||
"Thus it came about that a decade later the counterpart of Richard Whitney was\n",
|
||
"Alger Hiss.Each served admirably the enemies of his class. Each in origin,\n",
|
||
"education, associations, and career pretension epitomized that class. In each\n",
|
||
"case the first reaction of friends to the allegations of guilt was disbelief.\n",
|
||
"Whitney's past role in his community had been more prominent, and hence from the\n",
|
||
"viewpoint of his enemies he was a more satisfactory figure than Alger Hiss. In\n",
|
||
"the government hierarchy, Hiss was a distinctly routine figure. His eminence as\n",
|
||
"a global statesman was synthesized ex post facto and he also gained much\n",
|
||
"prominence during two long trials. Whitney, with no fanfare, accepted his\n",
|
||
"fate.There is, perhaps, a moral worth drawing from the careers of Whitney and\n",
|
||
"Hiss. Neither the fact that Whitney was convicted of purloining securities nor\n",
|
||
"that Hiss purloined documents is convincing proof that their friends,\n",
|
||
"associates, and contemporaries were doing the same. On the contrary, the\n",
|
||
"evidence would indicate that most brokers were honest as a matter of absolute\n",
|
||
"routine, and most New Dealers, so far from being in league with the Russians,\n",
|
||
"wished only that they might be invited once to taste caviar at the Soviet\n",
|
||
"embassy. Both liberals and conservatives, left and right, have now had personal\n",
|
||
"experience with the use of the symbolic evil. The injustice of the device is\n",
|
||
"evident. What may be a more compelling point, so are its dangers. In accordance\n",
|
||
"with an old but not outworn tradition, it might now be wise for all to conclude\n",
|
||
"that crime, or even misbehavior, is the act of an individual, not the\n",
|
||
"predisposition of a class. VIIThe Whitney affair brought a marked change\n",
|
||
"in the relations between the Exchange and the federal government, and, in some\n",
|
||
"measure, between the Exchange and the general public. In the Securities Act of\n",
|
||
"1933, and more comprehensively in the Securities Exchange Act of 1934, the\n",
|
||
"government had sought to prohibit some of the more spectacular extravagances of\n",
|
||
"1928 and 1929. Full disclosure was required on new security issues, although no\n",
|
||
"way was found of making would-be investors read what was disclosed. Inside\n",
|
||
"operations and short selling after the maimer of Mr. Wiggin were outlawed.\n",
|
||
"Authority was given to the Federal Reserve Board to fix margin requirements and\n",
|
||
"these could, if necessary, be made 100 per cent and thus eliminate margin\n",
|
||
"trading entirely. Pool operations, wash sales, the dissemination of tips or\n",
|
||
"patently false information and other devices for rigging or manipulating the\n",
|
||
"market were prohibited. Commercial banks were divorced from their securities\n",
|
||
"affiliates. Most important, the principle was enunciated that the New York Stock\n",
|
||
"Exchange and the other exchanges were subject to public regulation and the\n",
|
||
"Securities and Exchange Commission was established to apply and enforce such\n",
|
||
"regulation. This was somewhat bitter medicine. Moreover, regulatory bodies, like\n",
|
||
"the people who comprise them, have a marked life cycle. In youth they are\n",
|
||
"vigorous, aggressive, evangelistic, and even intolerant. Later they mellow, and\n",
|
||
"in old age—after a matter of ten or fifteen years—they become, with some\n",
|
||
"exceptions, either an arm of the industry they are regulating or senile. The SEC\n",
|
||
"was especially aggressive. To any young regulatory body, after all, Wall Street\n",
|
||
"was certain to seem a challenging antagonist.Until the Whitney affair Wall\n",
|
||
"Street—always with exceptions—was disposed to fight back. It insisted on the\n",
|
||
"right of a financial community in general, and of a securities market in\n",
|
||
"particular, to conduct its affairs in its own way, by its own lights and to\n",
|
||
"govern itself. On the evening before the suspension of Whitney was announced\n",
|
||
"from the rostrum, Charles R. Gay, the President of the Exchange, and Howland S.\n",
|
||
"Davis, Chairman of the Committee on Business Conduct—Whitney had been a\n",
|
||
"predecessor of both in the two offices—made their way to Washington. There they\n",
|
||
"reported their unhappy news to William O. Douglas and John W. Hanes of the SEC.\n",
|
||
"The trip, in far more than a symbolic sense, represented the surrender of the\n",
|
||
"Exchange. The cold war over regulation came to an end and was not thereafter\n",
|
||
"resumed. While the Whitney default confirmed the victory of the New Deal on the\n",
|
||
"issue of regulation and also served admirably to confirm the more general\n",
|
||
"suspicion of moral delinquency in downtown New York, it was Wall Street's good\n",
|
||
"fortune that it came late. By 1938 the New Deal assault on big business was on\n",
|
||
"the wane; some leaders of the original shock troops were already polishing up\n",
|
||
"speeches on the virtues of the free enterprise system. By then, also, it was\n",
|
||
"accepted New Deal theology that all necessary economic reforms had been revealed\n",
|
||
"and those that had not been enacted were on request from Congress. No further\n",
|
||
"reforms of the securities markets of any importance were on the agenda.\n",
|
||
"Henceforth Wall Street looked ingratiatingly at Washington and Washington merely\n",
|
||
"looked blank. CHAPTER IXCause and ConsequenceAFTER THE Great Crash came\n",
|
||
"the Great Depression which lasted, with varying severity, for ten years. In\n",
|
||
"1933, Gross National Product (total production of the economy) was nearly a\n",
|
||
"third less than in 1929. Not until 1937 did the physical volume of production\n",
|
||
"recover to the levels of 1929, and then it promptly slipped back again. Until\n",
|
||
"1941 the dollar value of production remained below 1929. Between 1930 and 1940\n",
|
||
"only once, in 1937, did the average number unemployed during the year drop below\n",
|
||
"eight million. In 1933 nearly thirteen million were out of work, or about one in\n",
|
||
"every four in the labor force. In 1938 one person in five was still out of\n",
|
||
"work.1It was dining this dreary time that 1929 became a year of myth. People\n",
|
||
"hoped that the country might get back to twenty-nine; in some industries or\n",
|
||
"towns when business was phenomenally good it was almost as good as in twenty-\n",
|
||
"nine; men of outstanding vision, on occasions of exceptional solemnity, were\n",
|
||
"heard to say that 1929 \"was no better than Americans deserve.\"On the whole, the\n",
|
||
"great stock market crash can be much more readily explained than the depression\n",
|
||
"that followed it. And among the problems involved in assessing the causes of\n",
|
||
"depression none is more intractable than the responsibility to be assigned to\n",
|
||
"the stock market crash. Economics still does not allow final answers on these\n",
|
||
"matters. But, as usual, something can be said. IIAs already so often\n",
|
||
"emphasized, the collapse in the stock market in the autumn of 1929 was implicit\n",
|
||
"in the speculation that went before. The only question concerning that\n",
|
||
"speculation was how long it would last. Sometime, sooner or later, confidence in\n",
|
||
"the short-run reality of increasing common stock values would weaken. When this\n",
|
||
"happened, some people would sell, and this would destroy the reality of\n",
|
||
"increasing values. Holding for an increase would now become meaningless; the new\n",
|
||
"reality would be falling prices. There would be a rush, pellmell, to unload.\n",
|
||
"This was the way past speculative orgies had ended. It was the way the end came\n",
|
||
"in 1929. It is the way speculation will end in the future.We do not know why a\n",
|
||
"great speculative orgy occurred in 1928 and 1929. The long accepted explanation\n",
|
||
"that credit was easy and so people were impelled to borrow money to buy common\n",
|
||
"stocks on margin is obviously nonsense. On numerous occasions before and since\n",
|
||
"credit has been easy, and there has been no speculation whatever. Furthermore,\n",
|
||
"much of the 1928 and 1929 speculation occurred on money borrowed at interest\n",
|
||
"rates which for years before, and in any period since, would have been\n",
|
||
"considered exceptionally astringent. Money, by the ordinary tests, was tight in\n",
|
||
"the late twenties.Far more important than rate of interest and the supply of\n",
|
||
"credit is the mood. Speculation on a large scale requires a pervasive sense of\n",
|
||
"confidence and optimism an' conviction that ordinary people were meant to be\n",
|
||
"rich. People must also have faith in the good intentions and even in the\n",
|
||
"benevolence of others, for it is by the agency of others that they will get\n",
|
||
"rich. In 1929 Professor Dice observed: \"The common folks believe in their\n",
|
||
"leaders. We no longer look upon the captains of industry as magnified crooks.\n",
|
||
"Have we not heard their voices over the radio? Are we not familiar with their\n",
|
||
"thoughts, ambitions, and ideals as they have expressed them to us almost as a\n",
|
||
"man talks to his friend?\"2 Such a feeling of trust is essential for a boom. When\n",
|
||
"people are cautious, questioning, misanthropic, suspicious, or mean, they are\n",
|
||
"immune to speculative enthusiasms. Savings must also be plentiful. Speculation,\n",
|
||
"however it may rely on borrowed funds, must be nourished in part by those who\n",
|
||
"participate. If savings are growing rapidly, people will place a lower marginal\n",
|
||
"value on their accumulation; they will be willing to risk some of it against the\n",
|
||
"prospect of a greatly enhanced return. Speculation, accordingly, is most likely\n",
|
||
"to break out after a substantial period of prosperity, rather than in the early\n",
|
||
"phases of recovery from a depression. Macaulay noted that between the\n",
|
||
"Restoration and the Glorious Revolution Englishmen were at loss to know what to\n",
|
||
"do with their savings and that the \"natural effect of this state of things was\n",
|
||
"that a crowd of projectors, ingenious and absurd, honest and knavish, employed\n",
|
||
"themselves in devising new schemes for the employment of redundant capital.\"\n",
|
||
"Bagehot and others have attributed the South Sea Bubble to roughly the same\n",
|
||
"causes.3 In 1720 England had enjoyed a long period of prosperity, enhanced in\n",
|
||
"part by war expenditures, and during this time private savings are believed to\n",
|
||
"have grown at an unprecedented rate. Investment outlets were also few and\n",
|
||
"returns low. Accordingly, Englishmen were anxious to place their savings at the\n",
|
||
"disposal of the new enterprises and were quick to believe that the prospects\n",
|
||
"were not fantastic. So it was in 1928 and 1929.Finally, a speculative outbreak\n",
|
||
"has a greater or less immunizing effect. The ensuing collapse automatically\n",
|
||
"destroys the very mood speculation requires. It follows that an outbreak of\n",
|
||
"speculation provides a reasonable assurance that another outbreak will not\n",
|
||
"immediately occur. With time and the dimming of memory, the immunity wears off.\n",
|
||
"A recurrence becomes possible. Nothing would have induced Americans to launch a\n",
|
||
"speculative adventure in the stock market in 1935. By 1955 the chances are very\n",
|
||
"much better. IIIAs noted, it is easier to account for the boom and crash\n",
|
||
"in the market than to explain their bearing on the depression which followed.\n",
|
||
"The causes of the Great Depression are still far from certain. A lack of\n",
|
||
"certainty, it may also be observed, is not evident in the contemporary writing\n",
|
||
"on the subject. Much of it tells what went wrong and why with marked firmness.\n",
|
||
"However, this paradoxically can itself be an indication of uncertainty. When\n",
|
||
"people are least sure they are often most dogmatic. We do not know what the\n",
|
||
"Russians intend, so we state with great assurance what they will do. We\n",
|
||
"compensate for our inability to foretell the consequences of, say, rearming\n",
|
||
"Germany by asserting positively just what the consequences will be. So it is in\n",
|
||
"economics. Yet, in explaining what happened in 1929 and after, one can\n",
|
||
"distinguish between explanations that might be right and those that are clearly\n",
|
||
"wrong. A great many people have always felt that a depression was inevitable in\n",
|
||
"the thirties. There had been (at least) seven good years; now by an occult or\n",
|
||
"biblical law of compensation there would have to be seven bad ones. Perhaps,\n",
|
||
"consciously or unconsciously, an argument that was valid for the stock market\n",
|
||
"was brought to bear on the economy in general. Because the market took leave of\n",
|
||
"reality in 1928 and 1929, it had at some time to make a return to reality. The\n",
|
||
"disenchantment was bound to be as painful as the illusions were beguiling.\n",
|
||
"Similarly, the New Era prosperity would some day evaporate; in its wake would\n",
|
||
"come the compensating hardship.There is also the slightly more subtle conviction\n",
|
||
"that economic life is governed by an inevitable rhythm. After a certain time\n",
|
||
"prosperity destroys itself and depression corrects itself. In 1929 prosperity,\n",
|
||
"in accordance with the dictates of the business cycle, had run its course. This\n",
|
||
"was the faith confessed by the members of the Harvard Economic Society in the\n",
|
||
"spring of 1929 when they concluded that a recession was somehow overdue.Neither\n",
|
||
"of these beliefs can be seriously supported. The twenties by being comparatively\n",
|
||
"prosperous established no imperative that the thirties be depressed. In the\n",
|
||
"past, good times have given way to less good times and less good or bad to good.\n",
|
||
"But change is normal in a capitalist economy. The degree of regularity in such\n",
|
||
"movements is not great, though often thought to be.4 No inevitable rhythm\n",
|
||
"required the collapse and stagnation of 1930–40.Nor was the economy of the\n",
|
||
"United States in 1929 subject to such physical pressure or strain as the result\n",
|
||
"of its past level of performance that a depression was bound to come. The notion\n",
|
||
"that the economy requires occasional rest and resuscitation has a measure of\n",
|
||
"plausibility and also a marked viability. During the summer of 1954 a\n",
|
||
"professional economist on President Eisenhower's personal staff explained the\n",
|
||
"then current recession by saying that the economy was enjoying a brief (and\n",
|
||
"presumably well-merited) rest after the exceptional exertions of preceding\n",
|
||
"years. In 1929 the labor force was not tired; it could have continued to produce\n",
|
||
"indefinitely at the best 1929 rate. The capital plant of the country was not\n",
|
||
"depleted. In the preceding years of prosperity, plant had been renewed and\n",
|
||
"improved. In fact, depletion of the capital plant occurred during the ensuing\n",
|
||
"years of idleness when new investment was sharply curtailed. Raw materials in\n",
|
||
"1929 were ample for the current rate of production. Entrepreneurs were never\n",
|
||
"more eupeptic. Obviously if men, materials, plant, and management were all\n",
|
||
"capable of continued and even enlarged exertions a refreshing pause was not\n",
|
||
"necessary. Finally, the high production of the twenties did not, as some have\n",
|
||
"suggested, outrun the wants of the people. During these years people were indeed\n",
|
||
"being supplied with an increasing volume of goods. But there is no evidence that\n",
|
||
"their desire for automobiles, clothing, travel, recreation, or even food was\n",
|
||
"sated. On the contrary, all subsequent evidence showed (given the income to\n",
|
||
"spend) a capacity for a large further increase in consumption. A depression was\n",
|
||
"not needed so that people's wants could catch up with their capacity to produce.\n",
|
||
"IVWhat, then, are the plausible causes of the depression? The task of answering\n",
|
||
"can be simplified somewhat by dividing the problem into two parts. First there\n",
|
||
"is the question of why economic activity turned down in 1929. Second there is\n",
|
||
"the vastly more important question of why, having started down, on this unhappy\n",
|
||
"occasion it went down and down and down and remained low for a full decade.As\n",
|
||
"noted, the Federal Reserve indexes of industrial activity and of factory\n",
|
||
"production, the most comprehensive monthly measures of economic activity then\n",
|
||
"available, reached a peak in June. They then turned down and continued to\n",
|
||
"decline throughout the rest of the year. The turning point in other\n",
|
||
"indicators—factory payrolls, freight-car loadings, and department store\n",
|
||
"sales—came later, and it was October or after before the trend in all of them\n",
|
||
"was clearly down. Still, as economists have generally insisted, and the matter\n",
|
||
"has the high authority of the National Bureau of Economic Research,5 the economy\n",
|
||
"had weakened in the early summer well before the crash.This weakening can be\n",
|
||
"variously explained. Production of industrial products, for the moment, had\n",
|
||
"outrun consumer and investment demand for them. The most likely reason is that\n",
|
||
"business concerns, in the characteristic enthusiasm of good times, misjudged the\n",
|
||
"prospective increase in demand and acquired larger inventories than they later\n",
|
||
"found they needed. As a result they curtailed their buying, and this led to a\n",
|
||
"cutback in production. In short, the summer of 1929 marked the beginning of the\n",
|
||
"familiar inventory recession. The proof is not conclusive from the (by present\n",
|
||
"standards) limited figures available. Department store inventories, for which\n",
|
||
"figures are available, seem not to have been out of line early in the year. But\n",
|
||
"a mild slump in department store sales in April could have been a signal for\n",
|
||
"curtailment. Also there is a chance—one that students of the period have\n",
|
||
"generally favored—that more deep-seated factors were at work and made themselves\n",
|
||
"seriously evident for the first time during that summer. Throughout the twenties\n",
|
||
"production and productivity per worker grew steadily: between 1919 and 1929,\n",
|
||
"output per worker in manufacturing industries increased by about 43 per cent.6\n",
|
||
"Wages, salaries, and prices all remained comparatively stable, or in any case\n",
|
||
"underwent no comparable increase. Accordingly, costs fell and with prices the\n",
|
||
"same, profits increased. These profits sustained the spending of the well-to-do,\n",
|
||
"and they also nourished at least some of the expectations behind the stock\n",
|
||
"market boom. Most of all they encouraged a very high level of capital\n",
|
||
"investment. During the twenties, the production of capital goods increased at an\n",
|
||
"average annual rate of 6.4 per cent a year; non-durable consumers' goods, a\n",
|
||
"category which includes such objects of mass consumption as food and clothing,\n",
|
||
"increased at a rate of only 2.8 per cent.7 (The rate of increase for durable\n",
|
||
"consumers' goods such as cars, dwellings, home furnishings, and the like, much\n",
|
||
"of it representing expenditures of the well-off to well-to-do, was 5.9 per\n",
|
||
"cent.) A large and increasing investment in capital goods was, in other words, a\n",
|
||
"principal device by which the profits were being spent.8 It follows that\n",
|
||
"anything that interrupted the investment oudays—anything, indeed, which kept\n",
|
||
"them from showing the necessary rate of increase—could cause trouble. When this\n",
|
||
"occurred, compensation through an increase in consumer spending could not\n",
|
||
"automatically be expected. The effect, therefore, of insufficient\n",
|
||
"investment—investment that failed to keep pace with the steady increase in\n",
|
||
"profits—could be falling total demand reflected in turn in falling orders and\n",
|
||
"output. Again there is no final proof of this point, for unfortunately we do not\n",
|
||
"know how rapidly investment had to grow to keep abreast of the current increase\n",
|
||
"in profits.9 However, the explanation is broadly consistent with the facts.\n",
|
||
"There are other possible explanations of the downturn. Back of the insufficient\n",
|
||
"advance in investment may have been the high interest rates. Perhaps, although\n",
|
||
"less probably, trouble was transmitted to the economy as a whole from some weak\n",
|
||
"sector like agriculture. Further explanations could be offered. But one thing\n",
|
||
"about this experience is clear. Until well along in the autumn of 1929 the\n",
|
||
"downturn was limited. The recession in business activity was modest and\n",
|
||
"underemployment relatively slight. Up to November it was possible to argue that\n",
|
||
"not much of anything had happened. On other occasions, as noted—in 1924 and 1927\n",
|
||
"and of late in 1949—the economy has undergone similar recession. But, unlike\n",
|
||
"these other occasions, in 1929 the recession continued and continued and got\n",
|
||
"violently worse. This is the unique feature of the 1929 experience. This is what\n",
|
||
"we need really to understand. VThere seems little question that in 1929,\n",
|
||
"modifying a famous cliché, the economy was fundamentally unsound. This is a\n",
|
||
"circumstance of first-rate importance. Many things were wrong, but five\n",
|
||
"weaknesses seem to have had an especially intimate bearing on the ensuing\n",
|
||
"disaster. They are:1) The bad distribution of income. In 1929 the rich were\n",
|
||
"indubitably rich. The figures are not entirely satisfactory, but it seems\n",
|
||
"certain that the 5 per cent of the population with the highest incomes in that\n",
|
||
"year received approximately one third of all personal income. The proportion of\n",
|
||
"personal income received in the form of interest, dividends, and rent—the\n",
|
||
"income, broadly speaking, of the well-to-do—was about twice as great as in the\n",
|
||
"years following the Second World War.10This highly unequal income distribution\n",
|
||
"meant that the economy was dependent on a high level of investment or a high\n",
|
||
"level of luxury consumer spending or both. The rich cannot buy great quantities\n",
|
||
"of bread. If they are to dispose of what they receive it must be on luxuries or\n",
|
||
"by way of investment in new plants and new projects. Both investment and luxury\n",
|
||
"spending are subject, inevitably, to more erratic influences and to wider\n",
|
||
"fluctuations than the bread and rent outlays of the $25-a-week workman. This\n",
|
||
"high-bracket spending and investment was especially susceptible, one may assume,\n",
|
||
"to the crushing news from the stock market in October of 1929. 2) The bad\n",
|
||
"corporate structure. In November 1929, a few weeks after the crash, the Harvard\n",
|
||
"Economic Society gave as a principal reason why a depression need not be feared\n",
|
||
"its reasoned judgment that \"business in most lines has been conducted with\n",
|
||
"prudence and conservatism.\"11 The fact was that American enterprise in the\n",
|
||
"twenties had opened its hospitable arms to an exceptional number of promoters,\n",
|
||
"grafters, swindlers, impostors, and frauds. This, in the long history of such\n",
|
||
"activities, was a kind of flood tide of corporate larceny.The most important\n",
|
||
"corporate weakness was inherent in the vast new structure of holding companies\n",
|
||
"and investment trusts. The holding companies controlled large segments of the\n",
|
||
"utility, railroad, and entertainment business. Here, as with the investment\n",
|
||
"trusts, was the constant danger of devastation by reverse leverage. In\n",
|
||
"particular, dividends from the operating companies paid the interest on the\n",
|
||
"bonds of upstream holding companies. The interruption of the dividends meant\n",
|
||
"default on the bonds, bankruptcy, and the collapse of the structure. Under these\n",
|
||
"circumstances, the temptation to curtail investment in operating plant in order\n",
|
||
"to continue dividends was obviously strong. This added to deflationary\n",
|
||
"pressures. The latter, in turn, curtailed earnings and helped bring down the\n",
|
||
"corporate pyramids. When this happened, even more retrenchment was inevitable.\n",
|
||
"Income was earmarked for debt repayment. Borrowing for new investment became\n",
|
||
"impossible. It would be hard to imagine a corporate system better designed to\n",
|
||
"continue and accentuate a deflationary spiral. 3) The bad banking structure.\n",
|
||
"Since the early thirties, a generation of Americans has been told, sometimes\n",
|
||
"with amusement, sometimes with indignation, often with outrage, of the banking\n",
|
||
"practices of the late twenties. In fact, many of these practices were made\n",
|
||
"ludicrous only by the depression. Loans which would have been perfectly good\n",
|
||
"were made perfectly foolish by the collapse of the borrower's prices or the\n",
|
||
"markets for his goods or the value of the collateral he had posted. The most\n",
|
||
"responsible bankers—those who saw that their debtors were victims of\n",
|
||
"circumstances far beyond their control and sought to help—were often made to\n",
|
||
"look the worst. The bankers yielded, as did others, to the blithe, optimistic,\n",
|
||
"and immoral mood of times but probably not more so. A depression such as that of\n",
|
||
"1929–32, were it to begin as this is written, would also be damaging to many\n",
|
||
"currently impeccable banking reputations.However, although the bankers were not\n",
|
||
"unusually foolish in 1929, the banking structure was inherently weak. The\n",
|
||
"weakness was implicit in the large numbers of independent units. When one bank\n",
|
||
"failed, the assets of others were frozen while depositors elsewhere had a\n",
|
||
"pregnant warning to go and ask for their money. Thus one failure led to other\n",
|
||
"failures, and these spread with a domino effect. Even in the best of times local\n",
|
||
"misfortune or isolated mismanagement could start such a chain reaction. (In the\n",
|
||
"first six months of 1929, 346 banks failed in various parts of the country with\n",
|
||
"aggregate deposits of nearly $115 million.)12 When income, employment, and\n",
|
||
"values fell as the result of a depression bank failures could quickly become\n",
|
||
"epidemic. This happened after 1929. Again it would be hard to imagine a better\n",
|
||
"arrangement for magnifying the effects of fear. The weak destroyed not only the\n",
|
||
"other weak, but weakened the strong. People everywhere, rich and poor, were made\n",
|
||
"aware of the disaster by the persuasive intelligence that their savings had been\n",
|
||
"destroyed. Needless to say, such a banking system, once in the convulsions of\n",
|
||
"failure, had a uniquely repressive effect on the spending of its depositors and\n",
|
||
"the investment of its clients.4) The dubious state of the foreign balance. This\n",
|
||
"is a familiar story. During the First World War, the United States became a\n",
|
||
"creditor on international account. In the decade following, the surplus of\n",
|
||
"exports over imports which once had paid the interest and principal on loans\n",
|
||
"from Europe continued. The high tariffs, which restricted imports and helped to\n",
|
||
"create this surplus of exports remained. However, history and traditional\n",
|
||
"trading habits also accounted for the persistence of the favorable balance, so\n",
|
||
"called.Before, payments on interest and principal had in effect been deducted\n",
|
||
"from the trade balance. Now that the United States was a creditor, they were\n",
|
||
"added to this balance. The latter, it should be said, was not huge. In only one\n",
|
||
"year (1928) did the excess of exports over imports come to as much as a billion\n",
|
||
"dollars; in 1923 and 1926 it was only about $375,000,000.13 However, large or\n",
|
||
"small, this difference had to be covered. Other countries which were buying more\n",
|
||
"than they sold, and had debt payments to make in addition, had somehow to find\n",
|
||
"the means for making up the deficit in their transactions with the United\n",
|
||
"States.During most of the twenties the difference was covered by cash—i.e., gold\n",
|
||
"payments to the United States—and by new private loans by the United States to\n",
|
||
"other countries. Most of the loans were to governments—national, state, or\n",
|
||
"municipal bodies—and a large proportion were to Germany and Central and South\n",
|
||
"America. The underwriters' margins in handling these loans were generous; the\n",
|
||
"public took them up with enthusiasm; competition for the business was keen. If\n",
|
||
"unfortunately corruption and bribery were required as competitive instruments,\n",
|
||
"these were used. In late 1927 Juan Leguia, the son of the President of Peru, was\n",
|
||
"paid $450,000 by J. and W. Seligman and Company and the National City Company\n",
|
||
"(the security affiliate of the National City Bank) for his services in\n",
|
||
"connection with a $50,000,000 loan which these houses marketed for Peru.14\n",
|
||
"Juan's services, according to later testimony, were of a rather negative sort.\n",
|
||
"He was paid for not blocking the deal. The Chase extended President Machado of\n",
|
||
"Cuba, a dictator with a marked predisposition toward murder, a generous personal\n",
|
||
"line of credit which at one time reached $200,000.15 Machado's son-in-law was\n",
|
||
"employed by the Chase. The bank did a large business in Cuban bonds. In\n",
|
||
"contemplating these loans, there was a tendency to pass quickly over anything\n",
|
||
"that might appear to the disadvantage of the creditor. Mr. Victor Schoepperle, a\n",
|
||
"vice-president of the National City Company with the responsibility for Latin\n",
|
||
"American loans, made the following appraisal of Peru as a credit prospect: \n",
|
||
"Peru: Bad debt record, adverse moral and political risk, bad internal debt\n",
|
||
"situation, trade situation about as satisfactory as that of Chile in the past\n",
|
||
"three years. Natural resources more varied. On economic showing Peru should go\n",
|
||
"ahead rapidly in the next 10 years.16 On such showing the National City Company\n",
|
||
"floated a $15,000,000 loan for Peru, followed a few months later by a\n",
|
||
"$50,000,000 loan, and some ten months thereafter by a $25,000,000 issue. (Peru\n",
|
||
"did prove a highly adverse political risk. President Leguia, who negotiated the\n",
|
||
"loans, was thrown violently out of office, and the loans went into default.)In\n",
|
||
"all respects these operations were as much a part of the New Era as Shenandoah\n",
|
||
"and Blue Ridge. They were also just as fragile, and once the illusions of the\n",
|
||
"New Era were dissipated they came as abruptly to an end. This, in turn, forced a\n",
|
||
"fundamental revision in the foreign economic position of the United States.\n",
|
||
"Countries could not cover their adverse trade balance with the United States\n",
|
||
"with increased payments of gold, at least not for long. This meant that they had\n",
|
||
"either to increase their exports to the United States or reduce their imports or\n",
|
||
"default on their past loans. President Hoover and the Congress moved promptly to\n",
|
||
"eliminate the first possibility—that the accounts would be balanced by larger\n",
|
||
"imports—by sharply increasing the tariff. Accordingly, debts, including war\n",
|
||
"debts, went into default and there was a precipitate fall in American exports.\n",
|
||
"The reduction was not vast in relation to total output of the American economy,\n",
|
||
"but it contributed to the general distress and was especially hard on farmers.5)\n",
|
||
"The poor state of economic intelligence. To regard the people of any time as\n",
|
||
"particularly obtuse seems vaguely improper, and it also establishes a precedent\n",
|
||
"which members of this generation might regret. Yet it seems certain that the\n",
|
||
"economists and those who offered economic counsel in the late twenties and early\n",
|
||
"thirties were almost uniquely perverse. In the months and years following the\n",
|
||
"stock market crash, the burden of reputable economic advice was invariably on\n",
|
||
"the side of measures that would make things worse. In November of 1929, Mr.\n",
|
||
"Hoover announced a cut in taxes; in the great no-business conferences that\n",
|
||
"followed he asked business firms to keep up their capital investment and to\n",
|
||
"maintain wages. Both of these measures were on the side of increasing spendable\n",
|
||
"income, though unfortunately they were largely without effect. The tax\n",
|
||
"reductions were negligible except in the higher income brackets; businessmen who\n",
|
||
"promised to maintain investment and wages, in accordance with a well-understood\n",
|
||
"convention, considered the promise binding only for the period within which it\n",
|
||
"was not financially disadvantageous to do so. As a result investment outlays and\n",
|
||
"wages were not reduced until circumstances would in any case have brought their\n",
|
||
"reduction. Still, the effort was in the right direction. Thereafter policy was\n",
|
||
"almost entirely on the side of making things worse. Asked how the government\n",
|
||
"could best advance recovery, the sound and responsible adviser urged that the\n",
|
||
"budget be balanced. Both parties agreed on this. For Republicans the balanced\n",
|
||
"budget was, as ever, high doctrine. But the Democratic Party platform of 1932,\n",
|
||
"with an explicitness which politicians rarely advise, also called for a \"federal\n",
|
||
"budget annually balanced on the basis of accurate executive estimates within\n",
|
||
"revenues...\"A commitment to a balanced budget is always comprehensive. It then\n",
|
||
"meant there could be no increase in government outlays to expand purchasing\n",
|
||
"power and relieve distress. It meant there could be no further tax reduction.\n",
|
||
"But taken literally it meant much more. From 1930 on the budget was far out of\n",
|
||
"balance, and balance, therefore, meant an increase in taxes, a reduction in\n",
|
||
"spending, or both. The Democratic platform in 1932 called for an \"immediate and\n",
|
||
"drastic reduction of governmental expenditures\" to accomplish at least a 25 per\n",
|
||
"cent decrease in the cost of government.The balanced budget was not a subject of\n",
|
||
"thought. Nor was it, as often asserted, precisely a matter of faith. Rather it\n",
|
||
"was a formula. For centuries avoidance of borrowing had protected people from\n",
|
||
"slovenly or reckless public housekeeping. Slovenly or reckless keepers of the\n",
|
||
"public purse had often composed complicated arguments to show why balance of\n",
|
||
"income and outlay was not a mark of virtue. Experience had shown that however\n",
|
||
"convenient this belief might seem in the short nm, discomfort or disaster\n",
|
||
"followed in the long run. Those simple precepts of a simple world did not hold\n",
|
||
"amid the growing complexities of the early thirties. Mass unemployment in\n",
|
||
"particular had altered the rules. Events had played a very bad trick on people,\n",
|
||
"but almost no one tried to think out the problem anew. The balanced budget was\n",
|
||
"not the only strait jacket on policy. There was also the bogey of \"going off\"\n",
|
||
"the gold standard and, most surprisingly, of risking inflation. Until 1932 the\n",
|
||
"United States added formidably to its gold reserves, and instead of inflation\n",
|
||
"the country was experiencing the most violent deflation in the nation's history.\n",
|
||
"Yet every sober adviser saw dangers here, including the danger of runaway price\n",
|
||
"increases. Americans, though in years now well in the past, had shown a penchant\n",
|
||
"for tinkering with the money supply and enjoying the brief but heady joys of a\n",
|
||
"boom in prices. In 1931 or 1932, the danger or even the feasibility of such a\n",
|
||
"boom was nil. The advisers and counselors were not, however, analyzing the\n",
|
||
"danger or even the possibility. They were serving only as the custodians of bad\n",
|
||
"memories.The fear of inflation reinforced the demand for the balanced budget. It\n",
|
||
"also limited efforts to make interest rates low, credit plentiful (or at least\n",
|
||
"redundant) and borrowing as easy as possible under the circumstances.\n",
|
||
"Devaluation of the dollar was, of course, flatly ruled out. This directly\n",
|
||
"violated the gold standard rules. At best, in such depression times, monetary\n",
|
||
"policy is a feeble reed on which to lean. The current economic clichés did not\n",
|
||
"allow even the use of that frail weapon. And again, these attitudes were above\n",
|
||
"party. Though himself singularly open-minded, Roosevelt was careful not to\n",
|
||
"offend or disturb his followers. In a speech in Brooklyn toward the close of the\n",
|
||
"1932 campaign, he said: The Democratic platform specifically declares, \"We\n",
|
||
"advocate a sound currency to be preserved at all hazards.\" That is plain\n",
|
||
"English. In discussing this platform on July 30, I said, \"Sound money is an\n",
|
||
"international necessity, not a domestic consideration for one nation alone.\" Far\n",
|
||
"up in the Northwest, at Butte, I repeated the pledge ... In Seattle I reaffirmed\n",
|
||
"my attitude...17 The following February, Mr. Hoover set forth his view, as often\n",
|
||
"before, in a famous letter to the President-elect: It would steady the country\n",
|
||
"greatly if there could be prompt assurance that there will be no tampering or\n",
|
||
"inflation of the currency; that the budget will be unquestionably balanced even\n",
|
||
"if further taxation is necessary; that the Government credit will be maintained\n",
|
||
"by refusal to exhaust it in the issue of securities.18 The rejection of both\n",
|
||
"fiscal (tax and expenditure) and monetary policy amounted precisely to a\n",
|
||
"rejection of all affirmative government economic policy. The economic advisers\n",
|
||
"of the day had both the unanimity and the authority to force the leaders of both\n",
|
||
"parties to disavow all the available steps to check deflation and depression. In\n",
|
||
"its own way this was a marked achievement—a triumph of dogma over thought. The\n",
|
||
"consequences were profound. VIIt is in light of the above weaknesses of\n",
|
||
"the economy that the role of the stock market crash in the great tragedy of the\n",
|
||
"thirties must be seen. The years of self-depreciation by Wall Street to the\n",
|
||
"contrary, the role is one of respectable importance. The collapse in securities\n",
|
||
"values affected in the first instance the wealthy and the well-to-do. But we see\n",
|
||
"that in the world of 1929 this was a vital group. The members disposed of a\n",
|
||
"large proportion of the consumer income; they were the source of a lion's share\n",
|
||
"of personal saving and investment. Anything that struck at the spending or\n",
|
||
"investment by this group would of necessity have broad effects on expenditure\n",
|
||
"and income in the economy at large. Precisely such a blow was struck by the\n",
|
||
"stock market crash. In addition, the crash promptly removed from the economy the\n",
|
||
"support that it had been deriving from the spending of stock market gains.The\n",
|
||
"stock market crash was also an exceptionally effective way of exploiting the\n",
|
||
"weaknesses of the corporate structure. Operating companies at the end of the\n",
|
||
"holding-company chain were forced by the crash to retrench. The subsequent\n",
|
||
"collapse of these systems and also of the investment trusts effectively\n",
|
||
"destroyed both the ability to borrow and the willingness to lend for investment.\n",
|
||
"What have long looked like purely fiduciary effects were, in fact, quickly\n",
|
||
"translated into declining orders and increasing unemployment. The crash was also\n",
|
||
"effective in bringing to an end the foreign lending by which the international\n",
|
||
"accounts had been balanced. Now the accounts had, in the main, to be balanced by\n",
|
||
"reduced exports. This put prompt and heavy pressure on export markets for wheat,\n",
|
||
"cotton, and tobacco. Perhaps the foreign loans had only delayed an adjustment in\n",
|
||
"the balance which had one day to come. The stock market crash served nonetheless\n",
|
||
"to precipitate the adjustment with great suddenness at a most unpropitious time.\n",
|
||
"The instinct of farmers who traced their troubles to the stock market was not\n",
|
||
"totally misguided.Finally, when the misfortune had struck, the attitudes of the\n",
|
||
"time kept anything from being done about it. This, perhaps, was the most\n",
|
||
"disconcerting feature of all. Some people were hungry in 1930 and 1931 and 1932.\n",
|
||
"Others were tortured by the fear that they might go hungry. Yet others suffered\n",
|
||
"the agony of the descent from the honor and respectability that goes with income\n",
|
||
"into poverty. And still others feared that they would be next. Meanwhile\n",
|
||
"everyone suffered from a sense of utter hopelessness. Nothing, it seemed, could\n",
|
||
"be done. And given the ideas which controlled policy, nothing could be done.Had\n",
|
||
"the economy been fundamentally sound in 1929 the effect of the great stock\n",
|
||
"market crash might have been small. Alternatively, the shock to confidence and\n",
|
||
"the loss of spending by those who were caught in the market might soon have worn\n",
|
||
"off. But business in 1929 was not sound; on the contrary it was exceedingly\n",
|
||
"fragile. It was vulnerable to the kind of blow it received from Wall Street.\n",
|
||
"Those who have emphasized this vulnerability are obviously on strong ground. Yet\n",
|
||
"when a greenhouse succumbs to a hailstorm something more than a purely passive\n",
|
||
"role is normally attributed to the storm. One must accord similar significance\n",
|
||
"to the typhoon which blew out of lower Manhattan in October 1929. VIIThe\n",
|
||
"military historian when he has finished his chronicle is excused. He is not\n",
|
||
"required to consider the chance for a renewal of war with the Indians, the\n",
|
||
"Mexicans, or the Confederacy. Nor will anyone press him to say how such acrimony\n",
|
||
"can be prevented. But economics is taken more seriously. The economic historian,\n",
|
||
"as a result, is invariably asked whether the misfortunes he describes will\n",
|
||
"afflict us again and how they may be prevented.The task of this book, as\n",
|
||
"suggested on an early page, is only to tell what happened in 1929. It is not to\n",
|
||
"tell whether or when the misfortunes of 1929 will recur. One of the pregnant\n",
|
||
"lessons of that year will by now be plain: it is that very specific and personal\n",
|
||
"misfortune awaits those who presume to believe that the future is revealed to\n",
|
||
"them. Yet, without undue risk, it may be possible to gain from our view of this\n",
|
||
"useful year some insights into the future. We can distinguish, in particular,\n",
|
||
"between misfortunes that could happen again and others which events, many of\n",
|
||
"them in the aftermath of 1929, have at least made improbable. And we can perhaps\n",
|
||
"see a little of the form and magnitude of the remaining peril.At first glance\n",
|
||
"the least probable of the misadventures of the late twenties would seem to be\n",
|
||
"another wild boom in the stock market with its inevitable collapse. As those\n",
|
||
"days of disenchantment drew to a close, tens of thousands of Americans shook\n",
|
||
"their heads and muttered, \"Never again.\" In every considerable community there\n",
|
||
"are, even now, a few survivors, aged but still chastened, who are still\n",
|
||
"muttering and still shaking their heads. The New Era had no such guardians of\n",
|
||
"sound pessimism. Also, there are the new government measures and controls. The\n",
|
||
"powers of the Federal Reserve Board—now styled the Board of Governors, the\n",
|
||
"Federal Reserve System—have been strengthened both in relation to the individual\n",
|
||
"Reserve banks and the member banks. Mitchell's defiance of March 1929 is now\n",
|
||
"unthinkable. What was then an act of arrogant but not abnormal individualism\n",
|
||
"would now be regarded as idiotic. The New York Federal Reserve Bank retains a\n",
|
||
"measure of moral authority and autonomy, but not enough to resist any Washington\n",
|
||
"policy. Now also there is power to set margin requirements. If necessary, the\n",
|
||
"speculator can be made to post the full price of the stock he buys. While this\n",
|
||
"may not completely discourage him, it does mean that when the market falls there\n",
|
||
"can be no outsurge of margin calls to force further sales and insure that the\n",
|
||
"liquidation will go through continuing spasms. Finally, the Securities and\n",
|
||
"Exchange Commission is a bar, one hopes effective, to large-scale market\n",
|
||
"manipulation, and it also keeps rein on the devices and the salesmanship by\n",
|
||
"which new speculators are recruited.Yet, in some respects, the chance for\n",
|
||
"recurrence of a speculative orgy remains good. No one can doubt that the\n",
|
||
"American people remain susceptible to the speculative mood—to the conviction\n",
|
||
"that enterprise can be attended by unlimited rewards in which they,\n",
|
||
"individually, were meant to share. A rising market can still bring the reality\n",
|
||
"of riches. This, in turn, can draw more and more people to participate. The\n",
|
||
"government preventatives and controls are ready. In the hands of a determined\n",
|
||
"government their efficacy cannot be doubted. There are, however, a hundred\n",
|
||
"reasons why a government will determine not to use them. In our democracy an\n",
|
||
"election is in the offing even on the day after an election. The avoidance of\n",
|
||
"depression and the prevention of unemployment have become for the politician the\n",
|
||
"most critical of all questions of public policy. Action to break up a boom must\n",
|
||
"always be weighed against the chance that it will cause unemployment at a\n",
|
||
"politically inopportune moment. Booms, it must be noted, are not stopped until\n",
|
||
"after they have started. And after they have started the action will always\n",
|
||
"look, as it did to the frightened men in the Federal Reserve Board in February\n",
|
||
"1929, like a decision in favor of immediate as against ultimate death. As we\n",
|
||
"have seen, the immediate death not only has the disadvantage of being immediate\n",
|
||
"but of identifying the executioner.The market will not go on a speculative\n",
|
||
"rampage without some rationalization. But during any future boom some newly\n",
|
||
"rediscovered virtuosity of the free enterprise system will be cited. It will be\n",
|
||
"pointed out that people are justified in paying the present prices—indeed,\n",
|
||
"almost any price—to have an equity position in the system. Among the first to\n",
|
||
"accept these rationalizations will be some of those responsible for invoking the\n",
|
||
"controls. They will say firmly that controls are not needed. The newspapers,\n",
|
||
"some of them, will agree and speak harshly of those who think action might be in\n",
|
||
"order. They will be called men of little faith.19 VIIIA new adventure in\n",
|
||
"stock market speculation sometime in the future followed by another collapse\n",
|
||
"would not have the same effect on the economy as in 1929. Whether it would show\n",
|
||
"the economy to be fundamentally sound or unsound is something, unfortunately,\n",
|
||
"that will not be wholly evident until after the event. There can be no question,\n",
|
||
"however, that many of the points of extreme weakness exposed in 1929 or soon\n",
|
||
"thereafter have since been substantially strengthened. The distribution of\n",
|
||
"income is no longer quite so lopsided. Between 1929 and 1948 the share of total\n",
|
||
"personal income going to the 5 per cent of the population with the highest\n",
|
||
"income dropped from nearly a third to less than a fifth of the total. Between\n",
|
||
"1929 and 1950 the share of all family income which was received as wages,\n",
|
||
"salaries, pensions, and unemployment compensation increased from approximately\n",
|
||
"61 per cent to approximately 71 per cent. This is the income of everyday people.\n",
|
||
"Although dividends, interest, and rent, the income characteristically of the\n",
|
||
"well-to-do, increased in total amount, the share dropped from just over 22 to\n",
|
||
"just over 12 per cent of total family personal income.20\" In ensuing years the\n",
|
||
"improvement in income distribution tapered off and slightly reversed itself. It\n",
|
||
"remains far better than in the twenties. Similarly after 1929, the great\n",
|
||
"investment trust promotions were folded up and put away, although eventually\n",
|
||
"they were replaced in part, and alas, by mutual and offshore funds, Equity\n",
|
||
"Funding and the Real Estate Investment Trusts, which became the casualties of\n",
|
||
"the post-1970 collapse. However, the SEC, aided by the bankruptcy laws,\n",
|
||
"flattened out the great utility holding company pyramids. Federal insurance of\n",
|
||
"bank deposits, even to this day, has not been given full credit for the\n",
|
||
"revolution that it has worked in the nation's banking structure. With this one\n",
|
||
"piece of legislation the fear which operated so efficiently to transmit weakness\n",
|
||
"was dissolved. As a result one grievous defect of the old system, by which\n",
|
||
"failure begot failure, was cured. Rarely has so much been accomplished by a\n",
|
||
"single law. The problem of the foreign balance is much changed from what it was\n",
|
||
"twenty-five years ago. Now the United States finds itself with a propensity to\n",
|
||
"buy or spend far more than it sells and receives.Finally, there has been a\n",
|
||
"modest accretion of economic knowledge. A developing depression would not now be\n",
|
||
"met with a fixed determination to make it worse. Without question, ceremonial\n",
|
||
"conferences would be assembled at the White House. We would see an explosion of\n",
|
||
"reassurance and incantation. Many would urge waiting and hoping as the best\n",
|
||
"policy. Not again, however, would people suppose that the best policy would\n",
|
||
"be—as Secretary Mellon so infelicitously phrased it—to \"liquidate labor,\n",
|
||
"liquidate stocks, liquidate the farmers, liquidate real estate.\"21 Our\n",
|
||
"determination to deal firmly and adequately with a serious depression is still\n",
|
||
"to be tested. But there is still a considerable difference between a failure to\n",
|
||
"do enough that is right and a determination to do much that is wrong.Other\n",
|
||
"weaknesses in the economy have been corrected. The much maligned farm program\n",
|
||
"provides a measure of security for farm income and therewith for spending by\n",
|
||
"farmers. Unemployment compensation accomplishes the same result, if still\n",
|
||
"inadequately, for labor. The remainder of the social security system—pensions\n",
|
||
"and public assistance—helps protect the income and consequently the expenditures\n",
|
||
"of yet other segments of the population. The tax system is a far better servant\n",
|
||
"of stability than it was in 1929. An angry god may have endowed capitalism with\n",
|
||
"inherent contradictions. But at least as an afterthought he was kind enough to\n",
|
||
"make social reform surprisingly consistent with improved operation of the\n",
|
||
"system. IXYet all this reinforcement notwithstanding, it would be unwise\n",
|
||
"to expose the economy to the shock of another major speculative collapse. Some\n",
|
||
"of the new reinforcements might buckle. Fissures might open at other new and\n",
|
||
"perhaps unexpected places. Even the quick withdrawal from the economy of the\n",
|
||
"spending that comes from stock market gains might be damaging. Any collapse,\n",
|
||
"even though the further consequences were small, would not be good for the\n",
|
||
"public reputation of Wall Street.Wall Street, in recent times, has become, as a\n",
|
||
"learned phrase has it, very \"public relations conscious.\" Since a speculative\n",
|
||
"collapse can only follow a speculative boom, one might expect that Wall Street\n",
|
||
"would lay a heavy hand on any resurgence of speculation. The Federal Reserve\n",
|
||
"would be asked by bankers and brokers to lift margins to the limit; it would be\n",
|
||
"warned to enforce the requirement sternly against those who might try to borrow\n",
|
||
"on their own stocks and bonds in order to buy more of them. The public would be\n",
|
||
"warned sharply and often of the risks inherent in buying stocks for the rise.\n",
|
||
"Those who persisted, nonetheless, would have no one to blame but themselves. The\n",
|
||
"position of the Stock Exchange, its members, the banks, and the financial\n",
|
||
"community in general would be perfectly clear and as well protected in the event\n",
|
||
"of a further collapse as sound public relations allow.As noted, all this might\n",
|
||
"logically be expected. However, it did not happen in the go-go years of the late\n",
|
||
"sixties and immediately after—the years of the performance funds and the\n",
|
||
"conglomerate explosion—nor will it come to pass. This is not because the\n",
|
||
"instinct for self-preservation in Wall Street is poorly developed. On the\n",
|
||
"contrary, it is probably normal and may be above. But now, as throughout\n",
|
||
"history, financial capacity and political perspicacity are inversely correlated.\n",
|
||
"Long-run salvation by men of business has never been highly regarded if it means\n",
|
||
"disturbance of orderly life and convenience in the present. So inaction will be\n",
|
||
"advocated in the present even though it means deep trouble in the future. Here,\n",
|
||
"at least equally with communism, lies the threat to capitalism. It is what\n",
|
||
"causes men who know that things are going quite wrong to say that things are\n",
|
||
"fundamentally sound. Index Acceptances, [>], [>]Agriculture. [>]; farm\n",
|
||
"program, [>]; and stock market, [>]; weakness of economy, [>]. See also\n",
|
||
"FarmersAktiebolaget Kreuger and Toll. See Kreuger and TollAldrich, Winthrop W.,\n",
|
||
"[>]–[>]All Quiet on the Western Front, [>]Alleghany Corporation, [>], [>]Allen,\n",
|
||
"Frederick Lewis, [>], [>] (n)., [>] (n)., [>] (n)., [>] (n)., [>], [>], [>],\n",
|
||
"[>], [>] (n)., [>] (n)., [>] (n).Allied Chemical and Dye, [>]American Can, [>],\n",
|
||
"[>]American Company, [>]American Founders Group, [>], [>]–[>], [>]American\n",
|
||
"Insuranstocks Corporation, [>]The American Magazine, [>]American Railway\n",
|
||
"Express, [>]American Stores, [>]American Telephone and Telegraph Company, [>],\n",
|
||
"[>], [>]–[>], [>], [>], [>], [>]–[>]American Tobacco, [>]American Trust Company,\n",
|
||
"[>]Anaconda Copper, [>]Anglo-American Shares, Inc., [>]Angly, Edward, [>] (n).,\n",
|
||
"[>] (n).Aquinas, Saint Thomas, [>]Armour and Company, [>]Arndt, H. W., [>]\n",
|
||
"(n).Associated Cas and Electric, [>], [>]Astor, Vincent, [>]The Atlantic\n",
|
||
"Monthly, [>]Auburn Automobile, [>]Axley, Seth, [>]Ayres, Colonel Leonard P.,\n",
|
||
"[>]Babe Ruth, [>]Babson, Roger W., [>], [>], [>]; Annual National Business\n",
|
||
"Conference, [>] Babson break, [>]–[>]; election forecast, [>]Bagehot, Walter,\n",
|
||
"[>], [>] (n), [>], [>]Baker, George F., Jr., [>] (n).Balance of payments. See\n",
|
||
"Foreign balanceBalanced budget, a formula, [>]–[>]; Whitney and, [>]Baldwin\n",
|
||
"Locomotive Works, [>]Bank of England, [>], [>]Bank of France, [>]Bankers, bear\n",
|
||
"raid denied, [>]–[>]; loss of prestige, [>]–[>], [>], [>]; optimism of, [>];\n",
|
||
"Pecora committee, [>]; speculation of officers in their own stocks, [>], [>],\n",
|
||
"[>]; support market, [>]–[>], [>]–[>]; support ended, [>]–[>], [>]Bankers Trust\n",
|
||
"Company, [>]Banks, failures, [>]; federal insurance of deposits, [>]; in another\n",
|
||
"crisis, [>]; out-of-town banks recall money from Wall Street, [>]; weakness of\n",
|
||
"banking structure, [>]–[>]. See also Bankers, Commercial banks, Federal Reserve\n",
|
||
"System, New York banksBarron's, [>], [>] (n)., [>], [>], [>]Barton, Bruce,\n",
|
||
"[>]Baruch, Bernard, [>]Bennett, John J., [>]Berlin, Irving, [>]Bethlehem Steel,\n",
|
||
"[>]Biddle, Nicholas, [>]Black Thursday. See New York Stock Exchange Blue Ridge\n",
|
||
"Corporation, [>], [>], [>], [>], [>], [>], [>], [>]Blyth and Company,\n",
|
||
"[>]Bonbright and Company, [>]Boston, [>], [>], [>], [>], [>]Boston Association\n",
|
||
"of Stock Exchange Firms, [>] (n).Boston Edison, [>]Boston Globe, [>]Brisbane,\n",
|
||
"Arthur, [>], [>], [>]Britain, general strike, [>]; gold standard, [>]–[>],\n",
|
||
"Restoration, [>]Brokers. [>], [>], [>], [>], [>]Brokers loans, [>], [>], [>],\n",
|
||
"[>], [>], [>]; explanation of, [>]; as index of volume of speculation, [>]–[>],\n",
|
||
"[>], [>]–[>], [>]–[>], [>]–[>], [>], [>], [>]; investment trusts, [>]; largest\n",
|
||
"weekly drop, [>]; warnings concerning, [>]Brookhart, Senator Smith W., [>],\n",
|
||
"[>]–[>]Brooklyn-Manhattan Trust Company, [>]Brownell, Herbert, [>]Bryan, William\n",
|
||
"Jennings, [>]Bull market, [>], [>], [>], [>], [>]. See also Stock marketBusiness\n",
|
||
"cycle: incantation and, [>]; law of compensation, [>], [>]–[>]; and speculation,\n",
|
||
"[>]Cabot, Paul C., [>]Caesar, Julius, [>]Call market. See Brokers' loansCanada,\n",
|
||
"Foshay, [>]; Wiggin, [>]Cantor Eddie, [>]Capitalism, [>]Carisbrooke, Marquess\n",
|
||
"of, [>]Carton, Sydney, [>]Case, J. I., [>], [>], [>], [>], [>]Catchings,\n",
|
||
"Waddill, [>]Central America, Foshay, [>]; loans to, [>]Central banking. See\n",
|
||
"Federal Reserve SystemCentral States Electric Corporation, [>], [>]Chain stores,\n",
|
||
"[>]Chase National Bank, [>], [>], [>], [>]; and Wiggin, [>]–[>]Chicago, Board of\n",
|
||
"Trade, [>]; gangsters, [>]–[>]; stock exchange, [>], [>]Chile, [>]Churchill,\n",
|
||
"Winston, [>], [>]Cincinnati, [>]Cities Service, [>]Clovis, New Mexico,\n",
|
||
"[>]Columbus, Ohio, [>]The Commercial Financial Chronicle, [>] (n) [>], [>] (n).,\n",
|
||
"[>], [>]Commercial banks, and Federal Reserve, [>]–[>], [>]–[>]; investment\n",
|
||
"trusts, [>]; loans, [>]–[>], [>]; and Mitchell, [>]; and securities affiliates,\n",
|
||
"[>]–[>], [>]Commercial National Bank and Trust Company, [>]Commercial Solvents,\n",
|
||
"[>]Commodity markets, [>]Common stocks, [>], [>]–[>], [>], [>]–[>], [>], [>],\n",
|
||
"[>]–[>], [>], [>], [>], [>]–[>]Commonwealth and Southern, [>]Communists, [>],\n",
|
||
"[>], [>]Congress, [>]–[>], [>], [>]; committees, [>]; investigations, [>],\n",
|
||
"[>]Consumer spending, [>], [>], [>], [>], [>]Continental Illinois Bank,\n",
|
||
"[>]Coolidge, Calvin, [>]–[>], [>], [>], [>], [>] [>]Coral Gables, [>]Com\n",
|
||
"Exchange Bank, [>]Corporate structure, [>]–[>], [>]Corporations, flow of funds\n",
|
||
"to market, loans in the market, [>]–[>]; recall funds, [>]Corsair,\n",
|
||
"[>]Cosmopolitan Fiscal Corporation, [>]Cotton, [>]County Trust Company,\n",
|
||
"[>]Couzens, Senator James, [>]–[>]Credit : easy credit not cause of speculation,\n",
|
||
"[>]; easy money policy, [>], [>], [>]; plentiful, [>]Crissinger, Daniel R.,\n",
|
||
"[>]Cummings, Attorney General Homer, [>]Curb Exchange, [>], [>], [>], [>],\n",
|
||
"[>]Cutten, Arthur W., [>], [>], [>], [>], [>]–[>]Cyrus, [>] Daily News (New\n",
|
||
"York), [>]Davis, Howland S., [>]Davis, Joseph S., [>]Day, Edmund E.,\n",
|
||
"[>]Deflation, [>], [>], [>]Democratic National Committee, [>], [>]Democrats,\n",
|
||
"[>], [>], [>]; and balanced budget, [>]; sound currency, [>]Department store\n",
|
||
"sales, [>]–[>]Depression [>], [>], [>], [>], [>], [>], [>]; Babson, [>]; Harvard\n",
|
||
"Economic Society, [>]–[>]; and market collapse, [>]; of 1920–[>], [>], [>], [>],\n",
|
||
"[>]. See also Business cycleDewey, Thomas E., [>], [>]–[>]Dice, Professor\n",
|
||
"Charles Amos, [>], [>] [>] (n)., [>], [>]–[>], [>]Distilled Liquors Corporation,\n",
|
||
"[>]–[>]Dividends [>], [>], [>]Dodsworth, [>]Dollar, devaluation of,\n",
|
||
"[>]–[>]Douglas, William O., [>]Dow-Jones averages, [>] (n)., [>]Dulles, John\n",
|
||
"Foster, [>]Du Pont (E. I. du Pont de Nemours and Company), [>]Du Ponts, [>].\n",
|
||
"[>]Durant, Donald, [>], [>] (n).Durant, William Crapo, [>], [>], [>], [>]Eastman\n",
|
||
"Kodak, [>]Economic history: insights, [>]–[>]; lessons of, [>]Economic policy,\n",
|
||
"added economic knowledge, [>]; inadequacy of, [>]–[>], [>]; and politics, [>]The\n",
|
||
"Economist, [>]Eisenhower, President Dwight D., [>]Election 1928, forecasts, [>];\n",
|
||
"Hoover victory and boom, [>]Electric Bond and Share Company, [>]Embezzlement,\n",
|
||
"boom and crash, [>]–[>]; Union Industrial Bank, [>]Employment, [>], [>], [>],\n",
|
||
"[>], [>]Equitable Trust Company, [>]–[>]Erleigh, Viscount, [>] (n).Export-Import\n",
|
||
"Bank, [>]Exports. See Foreign balanceFarley, James A., [>]Farmers, [>], [>]. See\n",
|
||
"also Agriculture.Federal Reserve Bank of Minneapolis, [>]Federal Reserve Bank of\n",
|
||
"New York, [>], [>]–[>], [>]–[>], [>]; rediscount rate, [>], [>], [>]–[>],\n",
|
||
"[>]Federal Reserve Board. See Federal Reserve System, BoardFederal Reserve\n",
|
||
"Bulletin, [>] (n).Federal Reserve index of factory production, [>], [>]Federal\n",
|
||
"Reserve index of industrial production, [>], [>], [>], [>], 1974Federal Reserve\n",
|
||
"System; Board of Governors, [>]; in another crisis, [>]; loans to New York\n",
|
||
"banks, [>], [>]; margin requirements, [>], [>], [>], [>]; and Miller, [>];\n",
|
||
"mystique of central banking, [>]; new powers of, [>]; open market operations,\n",
|
||
"[>]–[>], [>]; rediscount rate, [>], [>]–[>], [>]; and Warburg, [>]; weekly\n",
|
||
"record of brokers' loans, [>], willful helplessness, [>]Federal Reserve System,\n",
|
||
"Board; Blame for speculation, [>]; 1920–[>] depression, [>]; Coolidge, [>];\n",
|
||
"criticism of, [>]–[>]; easy money policy, [>]; hands-off policy, [>]; Hoover,\n",
|
||
"[>]; March meetings, [>]–[>]; Mitchell, [>]–[>]; moral suasion, [>]–[>], [>];\n",
|
||
"primary role and incompetence, [>]–[>], [>] rediscount rate, [>], [>]Fess,\n",
|
||
"Simeon D., [>]Financial and Industrial Securities Corporation, [>]Financial\n",
|
||
"Counselor, [>]Financial Panic of 1907, [>], [>]First National Bank of New York,\n",
|
||
"[>] (n).Fisher brothers, [>]Fisher, Professor Irving, [>], [>], [>]–[>], [>],\n",
|
||
"[>], [>], [>] Florida, hurricanes, [>]–[>], [>]; land boom, [>]–[>], [>], [>],\n",
|
||
"[>]; trading in \"binders,\" [>]–[>]; Whitney, [>]Foch, Marshal, [>]Ford, Henry,\n",
|
||
"[>], [>]Ford Motor Company, reduced prices, [>]; shutdown, [>]Forecasts, [>],\n",
|
||
"[>], [>]–[>], [>], [>], [>], [>]–[>], [>]–[>]Foreign balance, [>]–[>], [>],\n",
|
||
"[>]Foreign loans. See Foreign balanceFoshay, failure of, [>]–[>]Foster, William\n",
|
||
"Z., [>]Freight-car loadings, [>], [>], [>], [>]Friday, Dr. David, [>]Garrett,\n",
|
||
"Garet, [>] (n).Gay, Charles R., [>], [>]General Electric Company, [>], [>], [>],\n",
|
||
"[>], [>], [>], [>], [>]General Motors Corporation, [>]–[>], [>], [>], [>]; Sloan\n",
|
||
"president of, [>]Germany, [>], [>], [>]Gifford, Walter, [>]Glass, Senator\n",
|
||
"Carter, [>], [>], [>]Gold and Babson, [>]; flow to New York, [>]Gold standard,\n",
|
||
"[>], [>], [>]–[>]; Britain and, [>]Goldman, Sachs and Company, [>] (n), [>],\n",
|
||
"[>], [>], [>], [>], [>]Goldman Sachs Trading Corporation, [>], [>] [>], [>],\n",
|
||
"[>]Goldsmith, Selma, [>] (n).Good, James W., [>]Government regulation, [>],\n",
|
||
"[>]–[>], [>]–[>], [>]–[>]; SEC, [>]–[>]Graf Zeppelin, [>]Great Crash,\n",
|
||
"recurrence, [>]–[>]; relationship to the depression, [>], [>]; special\n",
|
||
"characteristic of 1929, [>], [>]–[>]Great Depression causes of, [>]–[>];\n",
|
||
"relation to stock market crash, [>], [>]–[>]; ten years of, [>]Gresham's Law,\n",
|
||
"[>]Gross National Product, [>]Guaranty Trust Company, [>]Gude Winmill Trading\n",
|
||
"Corporation, [>]Hague, Mayor Frank, [>], [>]Hanes, John W., [>]Harding, Warren\n",
|
||
"G., [>]Harpers Magazine, [>] (n).Harris, Seymour E., [>] (n).Harrison, George\n",
|
||
"L., [>]Harvard Economic Society, [>], [>], [>]–[>], [>], [>]Hatry, Clarence,\n",
|
||
"[>]–[>], [>]Hearst papers, Brisbane in, [>]Hiss Alger, [>]–[>]Holding company,\n",
|
||
"[>], [>]; bank holding companies, [>]; bad corporate structure, [>], [>]Home-\n",
|
||
"building industry, [>]Hoover, Herbert, [>], [>], [>] (n)., [>], [>], [>],\n",
|
||
"[>]–[>], [>], [>], [>]–[>], [>], [>], [>]; attempts at reassurance, [>]–[>];\n",
|
||
"balanced budget, [>]; concern over speculation, [>], [>]; no-business meetings,\n",
|
||
"[>]–[>], [>], [>]; tax cut, [>]Hopson, Howard C., [>], [>], [>], [>]Hornblower\n",
|
||
"and Weeks, [>], [>]–[>]Hugh-Jones, E. M., [>] (n).Incantation, [>], [>], [>],\n",
|
||
"[>]. See also Business cycleIncome, [>], [>], [>], [>]; distribution of, [>],\n",
|
||
"[>], [>]; farm income, [>]; state of economy in 1929, [>] [>]Incorporated\n",
|
||
"Investors, [>]Inflation, [>]Insult, Samuel, [>], [>], [>], [>], [>]Insull\n",
|
||
"Utility Investments, Inc., [>], [>]Interest rate, [>]; Federal Reserve, [>];\n",
|
||
"high interest rate, [>]; in margin trading, [>]; National City and, [>]; no\n",
|
||
"check to speculator, [>], [>]International Acceptance Bank, [>]International\n",
|
||
"Bank for Reconstruction and Development, [>]International Carriers, Ltd.,\n",
|
||
"[>]International Harvester, [>]International Nickel, [>]Inventory recession,\n",
|
||
"[>]–[>]Investment, [>], 1S6; failure to keep pace with profits, [>]; high\n",
|
||
"bracket, [>], [>] Investment banks, sponsors of investment trusts,\n",
|
||
"[>]–[>]Investment company. See Investment trustsInvestment corporation. See\n",
|
||
"Investment trustsInvestment News, [>] (n)., [>] (n).Investment trusts, [>],\n",
|
||
"[>]–[>], [>], [>], [>]; development and nature of. [>]–[>]; expansion, [>]–[>];\n",
|
||
"and genius, [>]–[>]; July 1932, [>]; Lehman Corporation, [>]; leverage, [>]–[>],\n",
|
||
"[>], [>], [>]–[>]; Raskob's plan, [>]–[>]; securities of, [>]–[>]; weaknesses\n",
|
||
"of, [>], [>]. See also Goldman, Sachs and Company; United Founders\n",
|
||
"CorporationInvestment Trusts and Investment Companies. See Securities and\n",
|
||
"Exchange CommissionIron Age, [>]Ivy League, [>]–[>].Jacksonville, [>]Jaszi,\n",
|
||
"George, [>] (n).Jay Cooke and Company, [>]Jefferson, Thomas, [>]Joint Committee\n",
|
||
"on the Economic Report, [>] (n).The Journal of Land and Public Utility\n",
|
||
"Economics, [>] (n).Journalists, skepticism toward boom, [>]; in pay of\n",
|
||
"speculators, [>]Kaitz, Hyman, [>] (n).Kemmerer, Professor Edwin W., [>]Keynes,\n",
|
||
"John Maynard, [>], [>] (n).Kingdom of Yugoslavia, [>]Klein, Dr. Julius,\n",
|
||
"[>]Knight, Peter O., [>]Kolo Products Corporation, [>]Kreuger, Ivar, [>], [>],\n",
|
||
"[>], [>], [>], [>]Kreuger and Toll, [>]–[>], [>], [>]Ladies' Home Journal, [>]La\n",
|
||
"Guardia, F. H., [>]Lamont, Robert P., Secretary of Commerce, [>]Lamont, Thomas\n",
|
||
"W., [>], [>], [>]–[>], [>]Latin America, [>]. See also Central and South\n",
|
||
"AmericaLawes, Warden of Sing Sing, [>]Lawrence, Joseph Stagg, [>]–[>], [>], [>],\n",
|
||
"[>]Lee, Higginson and Company, [>], [>] (n).Lef£vre, Edwin, [>], [>] (n).Leguia,\n",
|
||
"President of Peru, [>]Leguia, Juan, [>]Lehman Corporation, [>]Lehman, Herbert,\n",
|
||
"[>]Lenin, [>]Levenson, John J., [>]Leverage. See Investment trustsLiberty bonds,\n",
|
||
"[>]Liebenberg, Maurice, [>] (n).Lindbergh, Charles A., [>]Lion, David M., [>]The\n",
|
||
"Literary Digest, [>] (n)., [>], [>] (n)., [>], [>] (n).Livermore, Jesse L., [>],\n",
|
||
"[>]London, penny press, [>]Macaulay, Thomas Babington, [>]Machado, President of\n",
|
||
"Cuba, [>]Magazine of Wall Street, [>] (n).Manhattan Estates, [>]Manufacturing\n",
|
||
"establishments, number and value of output, [>]Marcosson, Isaac F., [>]Margin\n",
|
||
"trading, [>]–[>], [>]; amount of, [>]; brokers' loans, [>]–[>]; explanation and\n",
|
||
"uses of, [>]–[>]; and Federal Reserve, [>], [>], [>], [>]; margin calls,\n",
|
||
"[>]–[>], [>]; as speculative index, [>]. See also SpeculatorsMarion, Ohio,\n",
|
||
"[>]Mark Twain, [>], [>]Marne, Battle of, [>]Massachusetts Department of Public\n",
|
||
"Utilities, [>]–[>], [>]Master of Sempill, AFC, Colonel, [>]–[>]McAdoo, William\n",
|
||
"Gibbs, [>]McMahon Institute of Economic Research, [>]McMahon, William J.,\n",
|
||
"[>]McNeel's Financial Service, [>] Meehan, M. J., [>], [>], [>]–[>]Mellon,\n",
|
||
"Andrew W., [>]–[>], [>], [>], [>], [>], [>]Mergers, [>]–[>], [>], [>]; National\n",
|
||
"City and Com Exchange Bank, [>]Mexico, Foshay, [>]Miami, [>], [>], [>]Miami\n",
|
||
"Beach. [>]Midland Bank of Cleveland, [>]Miller, Adolph C., [>], [>]Minneapolis,\n",
|
||
"[>], [>]–[>]Mitchell, Charles E., [>], [>]–[>], [>], [>], [>], [>], [>]; arrest\n",
|
||
"[>]; and brokers' loans, [>]; Class A director, New York, Federal Reserve, [>];\n",
|
||
"defiance of Federal Reserve, [>]–[>], [>]; Morgan loan, [>]–[>]; resignation,\n",
|
||
"[>]; rumors of resignation, [>]; salary, [>]–[>]; tax liabilities, [>]–[>];\n",
|
||
"trial, [>]–[>]Mitchell, Wesley Clair, [>] (n).Montgomery Ward, [>], [>], [>],\n",
|
||
"[>], [>], [>]Moore, Geoffrey H., [>] (n).Morgan, J. P., [>], [>]Morgan, J. P.,\n",
|
||
"the elder, [>], [>]Morgan, J. P., and Company. [>]–[>], [>]–[>], [>]–[>];\n",
|
||
"Mitchells loan, [>]–[>]; and Whitney, [>], [>]Mount Pelee, [>]Murlyn\n",
|
||
"Corporation, [>]Mussolini, Benito, [>]Myers, William Starr, [>] (n).National\n",
|
||
"Bureau of Economic Research, [>]National City Bank, [>], [>]–[>], [>], [>], [>],\n",
|
||
"[>], [>]–[>], [>], [>]; merger with Corn Exchange, [>]–[>]; salaries of\n",
|
||
"officers, [>]–[>]; stock of, [>]–[>]National City Company, [>]National Republic\n",
|
||
"Investment Trust, [>]National Waterworks Corporation, [>]Nettie, city of, [>]New\n",
|
||
"Deal, [>], [>], [>], [>]New York banks, loans for margin trading, [>]–[>]; loans\n",
|
||
"from Federal Reserve, [>], [>]; loans increased during crisis, [>]–[>]. See also\n",
|
||
"Banks; Chase National Bank; National City BankNew York Central, [>]New York\n",
|
||
"Herald Tribune, [>] (n).New York Stock Exchange, [>], [>], [>], [>], [>]–[>],\n",
|
||
"[>], [>], [>]; Committee on Business Conduct, [>]; Committee on the Stock List,\n",
|
||
"[>]Days of panic: Black Thursday, [>]–[>], [>], [>]; Monday. October [>], [>];\n",
|
||
"Tuesday, October [>], [>]–[>], [>]Governing Committee, [>], [>]; impersonal\n",
|
||
"market, [>]–[>]; investigating Radio, [>]; and short selling, [>]; investigation\n",
|
||
"of, [>]–[>]; investment trusts, [>]; Kreuger's death, news withheld, [>];\n",
|
||
"manipulation, [>]; and member firms, [>]; organization of, [>]; and other\n",
|
||
"exchanges, [>]–[>]; question of closing, [>]–[>]; regulation of, (SEC), [>];\n",
|
||
"short sessions, [>], [>]–[>]; symbol of evil, [>], [>]; trading volume, [>],\n",
|
||
"[>], [>]–[>], [>], [>], [>]–[>], [>], [>]–[>], [>]–[>] [>], [>]–[>], [>]–[>],\n",
|
||
"[>], [>], [>]. [>]; transoceanic brokerage, [>]–[>]. See also Wall Street, Stock\n",
|
||
"marketNew York Stock Exchange Year Book, [>] (n)– [>] (n)., [>] (n).; tabulation\n",
|
||
"of brokers' loans, [>]New York Times, [>], [>], [>], [>] (n)., [>] (n)., [>],\n",
|
||
"[>], [>], [>], [>], [>] (n)., [>], [>], [>], [>], [>]New York Times industrial\n",
|
||
"averages, [>], [>] (n). (in 1924); [>], [>] (1925–27); [>], [>] (in 1928); [>],\n",
|
||
"[>], [>], [>], [>]–[>], [>], [>], [>], [>], [>], [>], [>], [>], [>] (in 1929),\n",
|
||
"[>] (in 1932)New York World, [>] (n).Newton, Walter H., [>] (n).No-business\n",
|
||
"meetings, [>]–[>], [>], [>]Norman, Montagu, [>], [>], [>]The North American\n",
|
||
"Review, [>]Noyes, Alexander Dana, [>] (n)., [>], [>] (n). Open market\n",
|
||
"operations. See Federal Reserve SystemOptions, [>]Organized support. See Stock\n",
|
||
"MarketOtis Elevator, [>]Pacific American Associates, [>]Palm Beach,\n",
|
||
"[>]Paramount-Famous-Lasky, [>]Paris, police, [>]Parker, Chauncey D.,\n",
|
||
"[>]–[>]Payne, Will, [>]Pecora Committee, [>] (n)., [>]. See also Senate\n",
|
||
"Committee on Banking and CurrencyPecora, Ferdinand, [>]Pennroad, [>]Peru,\n",
|
||
"[>]–[>]Philadelphia Record, [>]Pocantico Hills, [>]Ponzi, Charles, [>]–[>],\n",
|
||
"[>]Pools, [>], [>]–[>], [>], [>]–[>], [>]Poor's Weekly Business and Investment\n",
|
||
"Letter, [>]Potter, William C., [>]Prices, Fisher on stock prices, [>]; in the\n",
|
||
"twenties, [>], [>], [>]; October [>], [>]–[>]; prices allowed to fall, [>];\n",
|
||
"stocks dumped on market, [>]; support for stock prices, [>], [>]Princeton\n",
|
||
"University Press, [>]Production, [>], [>], [>]–[>]; automobiles, [>], [>]; coal,\n",
|
||
"[>]; during Depression, [>]; factory production, [>]; industrial production,\n",
|
||
"[>], [>], [>]; pig iron, [>], [>]; steel, [>], [>], [>], [>]Prohibition, [>],\n",
|
||
"[>]–[>], [>]Prosperity, [>]–[>], [>], [>], [>]; and speculation, [>]. See also\n",
|
||
"Business cycleProsser, Seward, [>]Proust, [>]Radice, E. A., [>] (n).Radio (\n",
|
||
"Radio Corporation of America), [>], [>], [>], [>], [>], [>], [>], [>]Raskob,\n",
|
||
"John J., [>]–[>], [>]–[>], [>], [>], [>]Rediscount rate. See Federal Reserve\n",
|
||
"SystemReis, Bernard J., [>] (n)., [>] (n).Reischsbank, [>]Republican National\n",
|
||
"Committee, [>]Republicans, [>], [>], [>]; balanced budget, [>], [>]; identified\n",
|
||
"with Wall Street, [>]; and Whitney, [>]Rhodes Cecil, [>]Richard Whitney and\n",
|
||
"Company. See Whitney, RichardRiordan, J. J., [>]–[>]Rist Charles, [>]Robbins,\n",
|
||
"Professor Lionel, [>], [>] (n)., [>] (n).Robinson, Henry M., [>]Rochester Gas\n",
|
||
"and Electric Company, [>]Rockefeller, John D., pegging Standard Oil, [>]–[>],\n",
|
||
"[>]; purchasing sound common stocks, [>], [>]Rockefeller, Percy A., [>],\n",
|
||
"[>]Rockefellers, [>]Roosevelt, Franklin D., [>]–[>], [>], [>], [>]Rosenwald,\n",
|
||
"Julius, [>]Royal Aeronautical Society, London, [>]Russia, [>], [>], [>]Sachs,\n",
|
||
"Walter E., [>]–[>]Sales against the box, [>]San Francisco, [>]Santa Fe,\n",
|
||
"[>]Saturday Evening Post, [>], [>] (n)., [>] (n).Savings, [>]; destroyed, [>];\n",
|
||
"and speculation, [>]–[>]Schacht, Hjalmar, [>], [>]Schoepperle, Victor,\n",
|
||
"[>]Schwab, Charles M., [>]Seaboard Air Line, [>], [>]–[>]Seaboard Utilities\n",
|
||
"Shares Corporation, [>]Securities Act of 1933, [>]Securities Exchange Act 1934,\n",
|
||
"[>], [>]Securities and Exchange Commission (SEC), [>], [>], [>], [>],\n",
|
||
"[>]Investment Trusts and Investment Companies, Report, [>] (n)., [>] (n)., [>]\n",
|
||
"(n)., [>] (n)., [>] (n)., [>] (n)., [>] (n)., [>] (n)., [>] (n). Securities and\n",
|
||
"Exchange Commission in the Matter of Richard Whitney, et al., [>] (n). [>] (n).,\n",
|
||
"[>] (n).Securities market. See Stock marketSeligman and Company, J. and W.,\n",
|
||
"[>]Senate, [>]; senators' salaries, [>]–[>]Senate Committee on Banking and\n",
|
||
"Currency, [>]Stock Exchange Practices, Hearings: [>] (n)., [>] (n)., [>] (n).,\n",
|
||
"[>] (n)., [>] (n)., [>]., [>] (n)., [>] (n)., [>] (n)., [>] (n)., [>]\n",
|
||
"(n).Report: [>] (n)., [>] (n)., [>] (n)., [>] (n)., [>] (n)., [>] (n)., [>]\n",
|
||
"(n)., [>] (n)., [>] (n)., [>] (n) [>] (n).Shenandoah Corporation, [>], [>], [>],\n",
|
||
"[>], [>], [>], [>]Shermar Corporation, [>]–[>]Short selling, [>]–[>], [>],\n",
|
||
"[>]Simmons Company, [>]Simmons, Edward H. H., [>] (n)., [>], [>]Sinclair\n",
|
||
"Consolidated Oil Company, [>], [>]Sinclair, Harry F., [>], [>], [>]Sloan, Alfred\n",
|
||
"P., Jr., [>], [>]Smith, Alfred E., [>], [>], [>]–[>]Smith, Bernard E., [>],\n",
|
||
"[>]Social Security, [>]Socialists, [>], [>]Solvay American Investment\n",
|
||
"Corporation, [>]South America, loans, [>]South Sea Bubble, [>], [>], [>], [>],\n",
|
||
"[>]Sparling, Earl, [>] (n).Speculation, [>], [>]–[>], [>]; boom begins in\n",
|
||
"earnest, [>]–[>]; brokers' loans index of, [>]–[>]; characteristics of\n",
|
||
"speculative periods, [>], [>]–[>], [>]–[>]. [>], [>]. [>], [>]–[>], [>]–[>],\n",
|
||
"collapse of, [>]; control of, [>]–[>] [>]; and Coolidge, [>]; devices of,\n",
|
||
"[>]–[>]; estimated participation in, [>]; exposure of, [>]; and Federal Reserve,\n",
|
||
"[>]–[>], [>]; Florida boom, [>]–[>], [>]–[>], [>]; and Hoover, [>]; immunizing\n",
|
||
"effect of collapse, [>], [>]; industrial stocks focus of, [>] (n).; reasons for\n",
|
||
"orgy, [>]–[>]; responsibility for crash, [>]; safeguards against [>]–[>]; and\n",
|
||
"Warburg, [>]. See also Margin trading, Speculators, Stock MarketSpeculators,\n",
|
||
"[>]–[>], [>], [>]–[>], [>], [>], [>], [>], [>], [>]. [>]–[>], [>], [>]; causing\n",
|
||
"crash, [>]–[>]; estimated number of, [>]–[>]; investigation of famous operators,\n",
|
||
"[>]; margin calls, [>]–[>], [>], [>], [>], [>], [>], [>]; panic, [>]; suicides,\n",
|
||
"[>], [>]–[>]Spokesman-Review, Spokane, [>]Standard Oil of New Jersey, [>], [>],\n",
|
||
"[>]–[>], [>]Standard Statistics Company, [>]State Street Investment Corporation,\n",
|
||
"[>]Stevens, Eugene M., [>]Stock exchanges, Boston, [>]; Buffalo, [>]; Chicago,\n",
|
||
"[>], [>]; out-of-town exchanges, [>], [>]; panic, [>]; regulation of (SEC),\n",
|
||
"[>]Stock exchange firms, [>]. [>]; exhaustion, [>], [>]; failures. [>], [>],\n",
|
||
"[>]; moral standards of, [>]; sponsors of investment trusts, [>]; transoceanic\n",
|
||
"brokerage, [>]–[>]. See also BrokersStock Exchange Practices. See Senate\n",
|
||
"Committee on Banking and CurrencyStock MarketBabson break, [>] big operators,\n",
|
||
"[>]–[>], [>]–[>]; Black Thursday, [>]–[>], [>]Boom: survived Florida, [>];\n",
|
||
"rising prices in twenties, [>]–[>]; March 1928, [>]–[>]; June 1928, [>];\n",
|
||
"election forecasts, [>]; post-election boom, [>]–[>]; February 1929 setback,\n",
|
||
"[>]; March nervousness, [>]–[>]; summer spurt, [>]Bull market ended, [>]; center\n",
|
||
"of immorality, [>]–[>], [>]; impersonal market, [>], [>]; 1930–[>], [>]–[>];\n",
|
||
"October prelude 1929, [>]–[>]; official optimism, sober predictions, [>]–[>];\n",
|
||
"organized support, [>]–[>], [>], [>]–[>], [>], [>], [>], [>]; panic, [>]–[>];\n",
|
||
"predominance of, [>]–[>]; reassurance, [>], [>], [>] (see also Incanta tion) as\n",
|
||
"reflection of economic situation, [>]; search for wrongdoers, [>], [>] [>];\n",
|
||
"slumps, [>]–[>], [>]. See also Margin trading, Speculators, Wall Street The\n",
|
||
"Stock Market Crash—and After, [>]Strong, Governor Benjamin, [>]–[>]Suicides,\n",
|
||
"Kreuger, [>]; number of, [>]–[>]; rate, [>]–[>]; Riordan. [>]–[>]Sullivan,\n",
|
||
"Lawrence, [>] (n).Syndicates, [>], [>], [>]–[>]Tammany Hall, [>]Tariffs, [>],\n",
|
||
"[>]Taxes, [>], [>], [>], [>]; avoidance of, [>]; and balanced budget; [>], [>];\n",
|
||
"Mitchell, [>]–[>]Teagle, Walter, [>], [>]Teapot Dome, [>]Television, [>]Time\n",
|
||
"Magazine, [>]\"The Trader\" [>]Trading in 'binders,\" [>]–[>]Transcontinental Air\n",
|
||
"Transport, [>]Tri-Continental Allied Corporation, [>]Truman, President Harry S,\n",
|
||
"[>]Trust Company of America, [>]Tucker, Dr. Rufus, [>]Union Industrial Bank of\n",
|
||
"Flint, Michigan, [>]–[>]United Corporation, [>], [>], [>], [>]United Founders\n",
|
||
"Corporation, [>], [>], [>], [>], [>]United States Department of\n",
|
||
"CommerceStatistical Abstract of the United States, [>] (n)., [>] (n).United\n",
|
||
"States Department of Health, Education and Welfare: Mortality Statistics, [>]\n",
|
||
"(n).Vital Statistics: Special Reports, [>]United States Steel Corporation, [>],\n",
|
||
"[>], [>], [>], [>]–[>], [>], [>], [>], [>], [>], [>]–[>], [>]; extra dividend,\n",
|
||
"[>]; Whitney's bid, [>]United States Treasury, [>]; Secretary of the Treasury,\n",
|
||
"[>]–[>]Vanderblue, Homer B., [>] (n)., [>] (n).Vauclain, Samuel, [>]Vest ris,\n",
|
||
"[>]Volume of trade. See New York Stock Exchange, Trading VolumeWages, [>], [>],\n",
|
||
"[>], [>]Walter, Mayor James J., [>], [>], [>]Wall Street, [>], [>], [>], [>],\n",
|
||
"[>]–[>], [>], [>], [>]–[>], [>], [>]–[>], [>], [>], [>], and Babson, [>];\n",
|
||
"bigness of, [>]–[>], [>], [>]–[>], [>]–[>]; criticism of, [>], [>]–[>], [>];\n",
|
||
"importance of, [>]; in another crisis, [>]–[>]; and Lawrence, [>]–[>]; loans at\n",
|
||
"[>] per cent, [>]; margin trading, [>]–[>]; new prestige of, [>], [>]; Number\n",
|
||
"[>], [>]; Raskob plan, [>]; relation to government, [>]–[>]; sponsoring\n",
|
||
"investment trusts, [>]; worst day, [>]–[>]. See also New York Stock Exchange,\n",
|
||
"Stock MarketWall Street and Washington, [>], [>] (n)., [>] (n)., [>] (n).Wall\n",
|
||
"Street Journal, [>], [>], [>] (n)., [>], [>], [>]–[>], [>], [>], [>]Warburg,\n",
|
||
"Paul M., [>]Waynoka, Oklahoma, [>]Wells, Sheldon Sinclair, [>]Western Utility\n",
|
||
"Investors, [>]Westinghouse, [>], [>], [>], [>], [>], [>]Whalen, Grover. [>],\n",
|
||
"[>]Wheat market, \"panic,\" [>]White Sewing Machine Company, [>]Whitney, George,\n",
|
||
"[>]Whitney, Richard, [>], [>] (n)– [>], [>] (n)., [>], [>]; appreciation of by\n",
|
||
"Stock Exchange, [>]; arrest, [>]; as a witness, [>]–[>]; buying to halt panic,\n",
|
||
"[>]–[>] [>], [>]; consequences for Stock Exchange, [>]–[>]; misfortunes of,\n",
|
||
"[>]–[>]Wiggin, Albert H– [>], [>], [>]; speculations, [>]–[>]Williams, Harrison,\n",
|
||
"[>], [>], [>] Wilson, Charles E., [>]Wilson, Senator from Indiana, [>]Wilson,\n",
|
||
"Thomas, [>] (n)., [>] (n)., [>] (n)., [>] (n)– [>] (n)., [>] (n).Women\n",
|
||
"investors, [>]–[>]Wool worth, [>]Wright Aeronautic, [>], [>], [>]Young, Owen D.,\n",
|
||
"[>]Young, Roy A., [>], [>] Footnotes1 U.S. Department of Commerce, Bureau\n",
|
||
"of the Census, Statistical Abstract of the United States, 1944–15.[back]***2\n",
|
||
"Federal Reserve Bulletin, December 1929.[back]***3 Thomas Wilson, Fluctuations\n",
|
||
"in Income and Employment, 3rd ed. (New York: Pitman, 1948), p. 141.[back]***4\n",
|
||
"These details are principally from two articles on the Florida land boom by\n",
|
||
"Homer B. Vanderblue in The Journal of Land and Public Utility Economics, May and\n",
|
||
"August 1927.[back]***5 Only Yesterday (New York: Harper, 1931), p. 280. Other\n",
|
||
"details of the damage resulting from the hurricane are from this still fresh and\n",
|
||
"lively book.[back]***6 Vanderblue, op. cit., p. 114.[back]***7 Allen, op. cit..\n",
|
||
"p. 282.[back]***8 Throughout this book I have used the New York Times industrial\n",
|
||
"averages as the short-hand designation of the level of security prices. This\n",
|
||
"series is the arithmetical, unweighted average of the prices of twenty-five of\n",
|
||
"what the Times describes as \"good, sound stocks with regular price changes and\n",
|
||
"generally active markets.\" The selection of the Times averages in preference to\n",
|
||
"the Dow-Jones or other averages was largely arbitrary. The Times averages are\n",
|
||
"the ones I have watched over the years; they are somewhat more accessible to the\n",
|
||
"non-professional observer than the Dow-Jones averages. Also, while the latter\n",
|
||
"are much better known, they carry in their wake a certain lore of market theory\n",
|
||
"which is irrelevant for present purposes. The industrial rather than the\n",
|
||
"railroad or combined average is cited because industrial stocks were the major\n",
|
||
"focus of speculation and displayed the widest amplitude of movement. Unless\n",
|
||
"there is indication to the contrary, values given are those at the close of the\n",
|
||
"market for the date indicated.[back]***9 Testimony before Senate Committee,\n",
|
||
"quoted by Lionel Robbins, The Great Depression (New York: Macmillan, 1934), p.\n",
|
||
"53.[back]***10 Ibid., p. 53.[back]***11 Allen, op. cit., p. 297.[back]***12\n",
|
||
"Understanding the New York Stock Exchange, 3rd ed. (New York: Stock Exchange,\n",
|
||
"April 1954), p. 2.[back]***13 New Levels in the Stock Market (New York: McGraw-\n",
|
||
"Hill, 1929), p. 9.[back]***14 Ibid., pp. 6–7.[back]***15 The Memoirs of Herbert\n",
|
||
"Hoover: The Great Depression, 1929–1941 (New York: Macmillan, 1952), p.\n",
|
||
"5.[back]***16 Ibid., p. 14.[back]***17 Dice, op. cit., p. 11.[back]***18 Year\n",
|
||
"Book, 1929–1930 (New York: Stock Exchange).[back]***19 The year-end figure was\n",
|
||
"$5,722,258,724. Figures are from the New York Stock Exchange Year Book,\n",
|
||
"1928–1929, and do not include brokers' time loans.[back]***20 Lombard Street,\n",
|
||
"1922 ed. (London: John Murray, 1922), p. 151.[back]***1 The Memoirs of Herbert\n",
|
||
"Hoover, p. 16.[back]***2 Ibid., p. 11.[back]***3 Ibid., p. 9.[back]***4 Ibid.,\n",
|
||
"p. 9, 10.[back]***5 Or the sale or reduction in inventory of commercial\n",
|
||
"paper.[back]***6 Stock Exchange Practices, Report of the Committee on Banking\n",
|
||
"and Currency pursuant to Senate Resolution 84 (Washington, 1934), p.\n",
|
||
"16.[back]***7 Ibid., pp. 13–14.[back]***8 Thomas Wilson, Fluctuations in Income\n",
|
||
"and Employment, p. 147.[back]***9 Ibid., pp. 147–48.[back]***10 Quoted by\n",
|
||
"Mitchell in Stock Exchange Practices, Hearings, Subcommittee, Senate Committee\n",
|
||
"on Banking and Currency—The Pecora Committee, February-March 1933, Pt. 6, p.\n",
|
||
"1817.[back]***11 Barron's, May 6, 1929.[back]***12 Wall Street and Washington\n",
|
||
"(Princeton: Princeton University Press, 1929), p. 3.[back]***13 Ibid., p.\n",
|
||
"v.[back]***14 The visit is described by Earl Sparling, Mystery Men of Wall\n",
|
||
"Street (New York: Greenberg, 1930), pp. 3–8. The authority is dubious, although\n",
|
||
"the author's facts, as distinct from his interpretation, are frequently\n",
|
||
"accurate.[back]***15 Barron's, June 10, 1929.[back]***16 Seymour E. Harris,\n",
|
||
"Twenty Years of Federal Reserve Policy (Cambridge: Harvard University Press,\n",
|
||
"1933), p. 547. I have made much use of this ultra-conservative but very careful\n",
|
||
"account of Federal Reserve policy.[back]***17 Hoover, Memoirs, p. 17.[back]***18\n",
|
||
"Ibid. Mr. Hoover, who is careless of such details, including his dates,\n",
|
||
"describes Whitney as President of the Exchange, which he only later\n",
|
||
"became.[back]***1 Walter Bagehot, Lombard Street, pp. 130, 131.[back]***2 One\n",
|
||
"estimate puts the number at about forty. Cf. Investment Trusts and Investment\n",
|
||
"Companies, Pt. I, Report of the Securities and Exchange Commission (Washington,\n",
|
||
"1939), p. 36.[back]***3 Ibid., p. 36.[back]***4 And would be more accurately\n",
|
||
"described as an investment company or investment corporation. However, I have\n",
|
||
"kept here to the less precise but more customary usage.[back]***5 The estimates\n",
|
||
"in this paragraph are all from Investment Trusts and Investment Companies, Pt.\n",
|
||
"III, Chap. 1, pp. 3, 4.[back]***6 Ibid., Pt. III, Chap. 2, p. 37 ff.[back]***7\n",
|
||
"Ibid., p. 39.[back]***8 Stock Exchange Practices. Report (Washington, 1934), pp.\n",
|
||
"103–4.[back]***9 The Literary Digest, June 1, 1929.[back]***10 Ibid.[back]***11\n",
|
||
"Wall Street and Washington, p. 163.[back]***12 Bernard J. Reis, False Security\n",
|
||
"(New York: Equinox, 1937). pp. 117 ff. and 296.[back]***13 Investment Trusts and\n",
|
||
"Investment Companies, Pt. I, p. 111.[back]***14 Ibid., Pt. I, pp. 61,\n",
|
||
"62.[back]***15 Ibid., Pt. III, Ch. I, p. 53.[back]***16 Assuming they were\n",
|
||
"reasonably orthodox. Bonds and preferred stock. In these days, were issues with\n",
|
||
"an almost infinite variety of conversion and participation rights.[back]***17\n",
|
||
"Part of it shared with Goldman, Sachs, as will be noted presently. The line\n",
|
||
"between a holding company, which has investment in and control of an operating\n",
|
||
"company (or another holding company), and an investment trust or company, which\n",
|
||
"has investment but which is presumed not to have control, is often a shadowy\n",
|
||
"one. The pyramiding of holding companies and concomitant leverage effects was\n",
|
||
"also a striking feature of the period.[back]***18 Investment Trusts and\n",
|
||
"Investment Companies, Pt. III, Ch. 1, pp. 5, 6[back]***19 Ibid., Pt. I, pp.\n",
|
||
"98–100.[back]***20 Reis, op. cit., p. 124.[back]***21 Stock Exchange Practices,\n",
|
||
"Hearings, April-June 1932, Pt. 2, pp. 566, 567.[back]***22 Details here are from\n",
|
||
"Investment Trusts and Investment Companies, Pt. III, Ch. 1, pp. 6 ff. and 17\n",
|
||
"ff.[back]***23 Details on Shenandoah, Blue Ridge, and the Pacific American\n",
|
||
"merger not from the New York Times of the period are from Investment Trusts md\n",
|
||
"Investment Companies, Pt. III, Ch. 1, pp. 5–7.[back]***24 E. H. H. Simmons, The\n",
|
||
"Principal Causes of the Stock Market Crisis of Nineteen Twenty-Nine (address\n",
|
||
"issued in pamphlet form by the New York Stock Exchange, January 1930), p.\n",
|
||
"16.[back]***25 Stock Exchange Practices, Hearings, April-June 1932, Pt. 2, pp.\n",
|
||
"566–67.[back]***1 Estimates are from Stock Exchange Practices, Report\n",
|
||
"(Washington, 1934), p. 8.[back]***2 The Wall Street Journal, September 19,\n",
|
||
"1929.[back]***3 New Levels in the Stock Market, p. 183.[back]***4 New York\n",
|
||
"Times, August 2, 1929.[back]***5 The American Mazagine, June 1929.[back]***6\n",
|
||
"Wall Street and Washington, p. 179. These passages were later quoted editorially\n",
|
||
"by the New York Times and are reproduced in turn from there.[back]***7 The\n",
|
||
"Commercial and Financial Chronicle, March 9. 1929, p. 1444.[back]***8 Alexander\n",
|
||
"Dana Noyes, The Market Place (Boston: Little, Brown, 1938), p. 324.[back]***9\n",
|
||
"Stock Exchange Practices. Hearings, April-June 1932, Pt. 2, pp. 601\n",
|
||
"ff.[back]***10 Ibid., p. 676 ff.[back]***11 Quoted by Allen, Only Yesterday, p.\n",
|
||
"322.[back]***12 Only Yesterday, p. 315.[back]***13 Stock Exchange Practices,\n",
|
||
"Report, 1934, pp. 9, 10.[back]***14 Noyes, op. cit., p. 328.[back]***15 Stock\n",
|
||
"Exchange Practices, Report, 1934, p. 30 ff.[back]***16 Viscount Erleigh, The\n",
|
||
"South Sea Bubble (New York: Putnam, 1933), p. 11.[back]***17 Noyes, op. cit., p.\n",
|
||
"328.[back]***18 The Literary Digest, August 31, 1929.[back]***19 Quoted in The\n",
|
||
"Literary Digest. August 31, 1929.[back]***20 \"One Day in History,\" Harper's\n",
|
||
"Magazine, November 1937.[back]***21 The Commercial and Financial Chronicle,\n",
|
||
"September 7, 1929.[back]***22 Quoted in The Wall Street Journal, September 6,\n",
|
||
"1929.[back]***23 Edward Angly, Oh, Yeah! (New York: Viking, 1931), p.\n",
|
||
"37.[back]***1 Thomas Wilson, Fluctuations in Income and Employment, p.\n",
|
||
"143.[back]***2 Hatry pleaded guilty and early in 1930 was given a long jail\n",
|
||
"sentence.[back]***3 Edwin Lefèvre, \"The Little Fellow in Wall Street,\" The\n",
|
||
"Saturday Evening Post, January 4, 1930.[back]***4 The amounts to be contributed\n",
|
||
"or otherwise committed were never specified. Frederick Lewis Allen (Only\n",
|
||
"Yesterday, pp. 329–30) says that each of the institutions, along with George F.\n",
|
||
"Baker, Jr., of the First National, who later joined the pool, put up $40\n",
|
||
"million. This total—$240 million—seems much too large to be plausible. The New\n",
|
||
"York Times subsequently suggested (March 9, 1938) that the total was some $20 to\n",
|
||
"$30 millions.[back]***5 Op. cit., p. 330.[back]***6 Quotations have normally\n",
|
||
"been rounded to the nearest whole number in this history. The steel quotation on\n",
|
||
"this day seems to call for an exception.[back]***7 This was stated by Garet\n",
|
||
"Garrett in The Saturday Evening Post (December 28, 1929) and it is generally\n",
|
||
"confirmed by Mr. Hoover in his memoirs. According to Mr. Garrett the banker's\n",
|
||
"consortium asked the President for the statement, which suggests that the\n",
|
||
"reassurance, like the support, was tolerably well organized.[back]***1 Only\n",
|
||
"Yesterday, p. 333.[back]***2 Allen, op. cit., p. 334.[back]***3 The Work of the\n",
|
||
"Stock Exchange in the Panic of 1929, an address by Richard Whitney before the\n",
|
||
"Boston Association of Stock Exchange Finns (Boston: June 10, 1930), pp. 16, 17.\n",
|
||
"Whitney's account, below, of the events of October 29 and thereafter is from the\n",
|
||
"same source.[back]***4 Caught Short! A Saga of Wailing Wall Street (New York:\n",
|
||
"Simon and Schuster, 1929 A.C. [After Crash]), p. 31.[back]***5 Investment News,\n",
|
||
"October 18, 1929, p. 538.[back]**** The Registration Area is the part of the\n",
|
||
"country—most of It—wherein causes of death are duly reported. Data are from\n",
|
||
"Vital Statistics: Special Reports, 1–45, 1935 (Washington: Department of\n",
|
||
"Commerce, Bureau of the Census, 1937).[back]***1 I am grateful to the custodians\n",
|
||
"of vital statistics in the Department of Health, Education and Welfare for\n",
|
||
"tracking, down these figures for me. They are from Mortality Statistics, 1929\n",
|
||
"(Washington: Department of Commerce, Bureau of the Census).[back]***2 Stock\n",
|
||
"Exchange Practices, January 1933, Pt. 4, p. 1214 ff. Such sales were heavy for\n",
|
||
"Friday and Saturday, but in the Exchange records of the time the two days are\n",
|
||
"not segregated. Mr. Donald Durant, the highly uninformed American director of\n",
|
||
"Kreuger and Toll, was in Paris at the time of Kreuger's death and cabled the\n",
|
||
"news to the firm of Lee, Higginson and Company, of which he was a partner. The\n",
|
||
"latter company, which was Kreuger's American investment banker, appears to have\n",
|
||
"refrained scrupulously from acting on the news. Ibid., pp. 1215–16.[back]***3\n",
|
||
"December 7, 1929.[back]***4 Lombard Street, page 150.[back]***5 Magazine of Wall\n",
|
||
"Street, December 14, 1929, p. 264. The reference to Foch and the Marne following\n",
|
||
"it from the same source.[back]***6 Both comments are from The Literary Digest,\n",
|
||
"November 30, 1929.[back]***7 July 9, 1932.[back]***8 Frederick Lewis Allen, Only\n",
|
||
"Yesterday, pp. 340–41.[back]***9 Quoted by Edward Angly, Oh, Yeah!, p. 27, from\n",
|
||
"the New York World October 15, 1930.[back]***1 Quotations are from the Weekly\n",
|
||
"Letters of the date given.[back]***2 New York Herald Tribune, November 3, 1929.\n",
|
||
"Quoted by The Commercial and Financial Chronicle, November 9, 1929.[back]***3\n",
|
||
"New York: Macmillan, 1930. The quotations following are on pages 53 and\n",
|
||
"269.[back]***4 Stock Exchange Practices, Report, 1934, p. 201–2.[back]***5 Stock\n",
|
||
"Exchange Practices, Hearings, October-November 1933, Pt. 6, pp. 2877\n",
|
||
"ff.[back]***6 Stock Exchange Practices, Report, 1934, pp. 192–93.[back]***7\n",
|
||
"Stock Exchange Practices, Report, 1934, pp. 188 ff.[back]***8 Stock Exchange\n",
|
||
"Practices, Hearings, October 1933, Pt. 5, p. 2304.[back]***9 Mr. Aldrich later\n",
|
||
"told a Senate committee (Ibid., p. 4020) that his differences of opinion with\n",
|
||
"friends of Mr. Wiggin, and presumably also with Mr. Wiggin, were a matter of\n",
|
||
"general knowledge.[back]***10 Ibid., p. 2302.[back]***11 Investment News,\n",
|
||
"November 16, 1929, p. 546.[back]***12 Stock Exchange Practices, Report, 1934, p.\n",
|
||
"206.[back]***13 Ibid., p. 322.[back]***14 Ibid., pp. 321, 322.[back]***15 Stock\n",
|
||
"Exchange Practices, Hearings, April 1932, Pt I, p. 1 ff.[back]***16 Stock\n",
|
||
"Exchange Practices, Hearings, February-March 1933, Pt. 6, pp. 2235\n",
|
||
"ff.[back]***17 These details are from Securities and Exchange Commission in the\n",
|
||
"Matter of Richard Whitney, Edwin D. Morgan, Etc., Vol. I, Report on\n",
|
||
"Investigation (Washington, 1938).[back]***18 Securities and Exchange Commission,\n",
|
||
"op. cit., Vol. II. p. 50.[back]***19 The New York Stock Exchange, an address by\n",
|
||
"Richard Whitney before the Industrial Club of St. Louis and the Chamber of\n",
|
||
"Commerce of St. Louis (St. Louis, September 27, 1932).[back]***20 Securities and\n",
|
||
"Exchange Commission, op. cit., Transcript of Hearings, Vol. II, pp. 822,\n",
|
||
"823.[back]***1 Economic Indicators: Historical and Descriptive Supplement, Joint\n",
|
||
"Committee on the Economic Report (Washington, 1953).[back]***2 New Levels in the\n",
|
||
"Stock Market, p. 257.[back]***3 Walter Bagehot, Lombard Street, p. 130. The\n",
|
||
"quotation from Macaulay. above, is cited by Bagehot, p. 128.[back]***4 \"At\n",
|
||
"present it is less likely that the existence of business cycles will be denied\n",
|
||
"than that their regularity will be exaggerated.\" Wesley Clair Mitchell, Business\n",
|
||
"Cycles and Unemployment (New York: McGraw-Hill, 1923), p. 6.[back]***5 Geoffrey\n",
|
||
"H. Moore, Statistical Indications of Cyclical Revivals and Recessions,\n",
|
||
"Occasional Paper 31, National Bureau of Economic Research, Inc. (New York.\n",
|
||
"1950).[back]***6 H. W. Arndt, The Economic Lessons of the Nineteen-Thirties\n",
|
||
"(London: Oxford, 1944), p. 15.[back]***7 E. M. Hugh-Jones and E. A. Radice, An\n",
|
||
"American Experiment (London: Oxford, 1936), 49. Cited by Arndt, op. cit., p.\n",
|
||
"16.[back]***8 This has been widely noted. See Lionel Robbins, The Great\n",
|
||
"Depression, p. 4, and Thomas Wilson, Fluctuations in Income, p. 154 ff., and J.\n",
|
||
"M. Keynes, A Treatise on Money (New York: Harcourt, Brace, 1930), II, 190\n",
|
||
"ff.[back]***9 Perhaps I may be permitted to enlarge on this in slightly more\n",
|
||
"technical terms. The interruption could as well have been caused by an\n",
|
||
"insufficient rate of increase in consumer spending as by a failure in the\n",
|
||
"greater rate of increase of capital goods spending. Under-consumption and under-\n",
|
||
"investment are the same side of the same coin. And some force is added to this\n",
|
||
"explanation by the fact that spending for one important consumers' durable,\n",
|
||
"namely houses, had been declining for several years and suffered a further\n",
|
||
"substantial drop in 1929. However, the investment function we still suppose to\n",
|
||
"be less stable than the consumption function, even though we are less assured of\n",
|
||
"the stability of the latter than we used to be. And in the present case it seems\n",
|
||
"wise to attach causal significance to the part of the spending which had to\n",
|
||
"maintain the largest rate of increase if total spending were to be\n",
|
||
"uninterrupted. The need to maintain a specific rate of increase in investment\n",
|
||
"outlay is insufficiently emphasized by Mr. Thomas Wilson in his book which I\n",
|
||
"have so frequently cited and to which students of the period are\n",
|
||
"indebted.[back]***10 Selma Goldsmith, George Jaszi, Hyman Kaitz, and Maurice\n",
|
||
"Liebenberg, \"Size Distribution of Income since the Mid-Thirties\" The Review of\n",
|
||
"Economics and Statistics, February 1954, pp. 16, 18.[back]***11 Weekly Letter,\n",
|
||
"November 23, 1929.[back]***12 Compiled from Federal Reserve Bulletin, monthly\n",
|
||
"issues, 1929.[back]***13 U.S. Department of Commerce, Bureau of Foreign and\n",
|
||
"Domestic Commerce, Statistical Abstract of the United States, 1942.[back]***14\n",
|
||
"Stock Exchange Practices, Report, 1934, pp. 220–21.[back]***15 Ibid., p.\n",
|
||
"215.[back]***16 Stock Exchange Practices, Hearings, February-March 1933, Pt. 6,\n",
|
||
"p. 2091 ff.[back]***17 Lawrence Sullivan, Prelude to Panic (Washington:\n",
|
||
"Statesman Press, 1936), p. 20.[back]***18 William Starr Myers and Walter H.\n",
|
||
"Newton, The Hoover Administration; A Documented Narrative (New York: Scribners,\n",
|
||
"1936), pp. 339–40.[back]***19 In warning in 1969 of the then current speculation\n",
|
||
"I managed to attract, in a modest way, this epithet.[back]***20 These data are\n",
|
||
"from Goldsmith, et al., \"Size of Distribution of Income,\" pp. 16, 18.[back]***21\n",
|
||
"Quoted by Herbert Hoover, Memoirs, p. 30.[back]\n",
|
||
"Text Statistics:\n",
|
||
"Word Count: 78191\n",
|
||
"Sentence Count: 3147\n",
|
||
"Average Word Length: 4.08\n",
|
||
"Text copied to clipboard.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import textwrap\n",
|
||
"import pyperclip\n",
|
||
"import pyperclip\n",
|
||
"import ebooklib \n",
|
||
"from ebooklib import epub\n",
|
||
"from bs4 import BeautifulSoup\n",
|
||
"\n",
|
||
"def extract_text_from_epub(epub_path, start_page=5, end_page=200, width=80):\n",
|
||
" book = epub.read_epub(epub_path)\n",
|
||
" text_content = []\n",
|
||
"\n",
|
||
" # Get all document items\n",
|
||
" document_items = [item for item in book.get_items() if item.get_type() == ebooklib.ITEM_DOCUMENT]\n",
|
||
"\n",
|
||
" # Restrict to the specified page range\n",
|
||
" for i, item in enumerate(document_items[start_page-1:end_page], start=start_page):\n",
|
||
" soup = BeautifulSoup(item.get_content(), 'html.parser')\n",
|
||
" text_content.append(soup.get_text())\n",
|
||
"\n",
|
||
" # Join all text content and wrap it\n",
|
||
" full_text = '\\n'.join(text_content)\n",
|
||
" wrapped_text = textwrap.fill(full_text, width=width)\n",
|
||
"\n",
|
||
" return wrapped_text\n",
|
||
"\n",
|
||
"# Example usage\n",
|
||
"fname = '/home/ys/Documents/The Great Crash 1929 - John Kenneth Galbraith.epub'\n",
|
||
"text = extract_text_from_epub(fname,start_page=5, end_page=100)\n",
|
||
"print(text)\n",
|
||
"\n",
|
||
"# Generate text statistics\n",
|
||
"stats = text_statistics(text)\n",
|
||
"print(\"Text Statistics:\")\n",
|
||
"print(f\"Word Count: {stats['word_count']}\")\n",
|
||
"print(f\"Sentence Count: {stats['sentence_count']}\")\n",
|
||
"print(f\"Average Word Length: {stats['average_word_length']:.2f}\")\n",
|
||
"\n",
|
||
"pyperclip.copy(text)\n",
|
||
"print(\"Text copied to clipboard.\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Extracting text from pdf files "
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"[nltk_data] Downloading package punkt to /home/ys/nltk_data...\n",
|
||
"[nltk_data] Package punkt is already up-to-date!\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Miguel Grinberg\n",
|
||
" Flask \n",
|
||
"Web Development\n",
|
||
"DEVELOPING WEB APPLICATIONS WITH PYTHON\n",
|
||
"2nd Edition\n",
|
||
"\n",
|
||
"\n",
|
||
"Miguel Grinberg\n",
|
||
"Flask Web Development\n",
|
||
"Developing Web Applications with Python\n",
|
||
"SECOND EDITION\n",
|
||
"Boston\n",
|
||
"Farnham\n",
|
||
"Sebastopol\n",
|
||
"Tokyo\n",
|
||
"Beijing\n",
|
||
"Boston\n",
|
||
"Farnham\n",
|
||
"Sebastopol\n",
|
||
"Tokyo\n",
|
||
"Beijing\n",
|
||
"\n",
|
||
"978-1-491-99173-2\n",
|
||
"[LSI]\n",
|
||
"Flask Web Development\n",
|
||
"by Miguel Grinberg\n",
|
||
"Copyright © 2018 Miguel Grinberg. All rights reserved.\n",
|
||
"Printed in the United States of America.\n",
|
||
"Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.\n",
|
||
"O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are\n",
|
||
"also available for most titles (http://oreilly.com/safari). For more information, contact our corporate/insti‐\n",
|
||
"tutional sales department: 800-998-9938 or corporate@oreilly.com.\n",
|
||
"Editor: Allyson MacDonald\n",
|
||
"Production Editor: Colleen Cole\n",
|
||
"Copyeditor: Dwight Ramsey\n",
|
||
"Proofreader: Rachel Head\n",
|
||
"Indexer: Ellen Troutman\n",
|
||
"Interior Designer: David Futato\n",
|
||
"Cover Designer: Randy Comer\n",
|
||
"Illustrator: Rebecca Demarest\n",
|
||
"March 2018:\n",
|
||
" Second Edition\n",
|
||
"Revision History for the Second Edition\n",
|
||
"2018-03-02: First Release\n",
|
||
"See http://oreilly.com/catalog/errata.csp?isbn=9781491991732 for release details.\n",
|
||
"The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Flask Web Development, the cover\n",
|
||
"image, and related trade dress are trademarks of O’Reilly Media, Inc.\n",
|
||
"While the publisher and the author have used good faith efforts to ensure that the information and\n",
|
||
"instructions contained in this work are accurate, the publisher and the author disclaim all responsibility\n",
|
||
"for errors or omissions, including without limitation responsibility for damages resulting from the use of\n",
|
||
"or reliance on this work. Use of the information and instructions contained in this work is at your own\n",
|
||
"risk. If any code samples or other technology this work contains or describes is subject to open source\n",
|
||
"licenses or the intellectual property rights of others, it is your responsibility to ensure that your use\n",
|
||
"thereof complies with such licenses and/or rights.\n",
|
||
"\n",
|
||
"For Alicia.\n",
|
||
"\n",
|
||
"\n",
|
||
"Table of Contents\n",
|
||
"Preface. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi\n",
|
||
"Part I. \n",
|
||
"Introduction to Flask\n",
|
||
"1. Installation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1\n",
|
||
"Creating the Application Directory 2\n",
|
||
"Virtual Environments 2\n",
|
||
"Creating a Virtual Environment with Python 3 3\n",
|
||
"Creating a Virtual Environment with Python 2 3\n",
|
||
"Working with a Virtual Environment 4\n",
|
||
"Installing Python Packages with pip 5\n",
|
||
"2. Basic Application Structure. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7\n",
|
||
"Initialization 7\n",
|
||
"Routes and View Functions 8\n",
|
||
"A Complete Application 9\n",
|
||
"Development Web Server 10\n",
|
||
"Dynamic Routes 12\n",
|
||
"Debug Mode 13\n",
|
||
"Command-Line Options 15\n",
|
||
"The Request-Response Cycle 17\n",
|
||
"Application and Request Contexts 17\n",
|
||
"Request Dispatching 18\n",
|
||
"The Request Object 19\n",
|
||
"Request Hooks 20\n",
|
||
"Responses 21\n",
|
||
"Flask Extensions 23\n",
|
||
"v\n",
|
||
"\n",
|
||
"3. Templates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25\n",
|
||
"The Jinja2 Template Engine 26\n",
|
||
"Rendering Templates 26\n",
|
||
"Variables 27\n",
|
||
"Control Structures 28\n",
|
||
"Bootstrap Integration with Flask-Bootstrap 30\n",
|
||
"Custom Error Pages 33\n",
|
||
"Links 36\n",
|
||
"Static Files 37\n",
|
||
"Localization of Dates and Times with Flask-Moment 38\n",
|
||
"4. Web Forms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43\n",
|
||
"Configuration 44\n",
|
||
"Form Classes 44\n",
|
||
"HTML Rendering of Forms 47\n",
|
||
"Form Handling in View Functions 48\n",
|
||
"Redirects and User Sessions 51\n",
|
||
"Message Flashing 53\n",
|
||
"5. Databases. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57\n",
|
||
"SQL Databases 57\n",
|
||
"NoSQL Databases 58\n",
|
||
"SQL or NoSQL? 59\n",
|
||
"Python Database Frameworks 59\n",
|
||
"Database Management with Flask-SQLAlchemy 61\n",
|
||
"Model Definition 62\n",
|
||
"Relationships 64\n",
|
||
"Database Operations 66\n",
|
||
"Creating the Tables 66\n",
|
||
"Inserting Rows 66\n",
|
||
"Modifying Rows 68\n",
|
||
"Deleting Rows 68\n",
|
||
"Querying Rows 68\n",
|
||
"Database Use in View Functions 71\n",
|
||
"Integration with the Python Shell 72\n",
|
||
"Database Migrations with Flask-Migrate 73\n",
|
||
"Creating a Migration Repository 73\n",
|
||
"Creating a Migration Script 74\n",
|
||
"Upgrading the Database 75\n",
|
||
"Adding More Migrations 76\n",
|
||
"vi \n",
|
||
"| \n",
|
||
"Table of Contents\n",
|
||
"\n",
|
||
"6. Email. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79\n",
|
||
"Email Support with Flask-Mail 79\n",
|
||
"Sending Email from the Python Shell 81\n",
|
||
"Integrating Emails with the Application 81\n",
|
||
"Sending Asynchronous Email 83\n",
|
||
"7. Large Application Structure. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85\n",
|
||
"Project Structure 85\n",
|
||
"Configuration Options 86\n",
|
||
"Application Package 88\n",
|
||
"Using an Application Factory 88\n",
|
||
"Implementing Application Functionality in a Blueprint 90\n",
|
||
"Application Script 93\n",
|
||
"Requirements File 93\n",
|
||
"Unit Tests 94\n",
|
||
"Database Setup 96\n",
|
||
"Running the Application 97\n",
|
||
"Part II. \n",
|
||
"Example: A Social Blogging Application\n",
|
||
"8. User Authentication. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101\n",
|
||
"Authentication Extensions for Flask 101\n",
|
||
"Password Security 102\n",
|
||
"Hashing Passwords with Werkzeug 102\n",
|
||
"Creating an Authentication Blueprint 105\n",
|
||
"User Authentication with Flask-Login 107\n",
|
||
"Preparing the User Model for Logins 107\n",
|
||
"Protecting Routes 108\n",
|
||
"Adding a Login Form 109\n",
|
||
"Signing Users In 111\n",
|
||
"Signing Users Out 112\n",
|
||
"Understanding How Flask-Login Works 113\n",
|
||
"Testing Logins 114\n",
|
||
"New User Registration 115\n",
|
||
"Adding a User Registration Form 115\n",
|
||
"Registering New Users 117\n",
|
||
"Account Confirmation 118\n",
|
||
"Generating Confirmation Tokens with itsdangerous 118\n",
|
||
"Sending Confirmation Emails 120\n",
|
||
"Account Management 125\n",
|
||
"Table of Contents \n",
|
||
"| \n",
|
||
"vii\n",
|
||
"\n",
|
||
"9. User Roles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127\n",
|
||
"Database Representation of Roles 127\n",
|
||
"Role Assignment 131\n",
|
||
"Role Verification 132\n",
|
||
"10. User Profiles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137\n",
|
||
"Profile Information 137\n",
|
||
"User Profile Page 138\n",
|
||
"Profile Editor 141\n",
|
||
"User-Level Profile Editor 141\n",
|
||
"Administrator-Level Profile Editor 143\n",
|
||
"User Avatars 146\n",
|
||
"11. Blog Posts. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151\n",
|
||
"Blog Post Submission and Display 151\n",
|
||
"Blog Posts on Profile Pages 154\n",
|
||
"Paginating Long Blog Post Lists 155\n",
|
||
"Creating Fake Blog Post Data 155\n",
|
||
"Rendering in Pages 157\n",
|
||
"Adding a Pagination Widget 158\n",
|
||
"Rich-Text Posts with Markdown and Flask-PageDown 161\n",
|
||
"Using Flask-PageDown 162\n",
|
||
"Handling Rich Text on the Server 164\n",
|
||
"Permanent Links to Blog Posts 165\n",
|
||
"Blog Post Editor 167\n",
|
||
"12. Followers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171\n",
|
||
"Database Relationships Revisited 171\n",
|
||
"Many-to-Many Relationships 172\n",
|
||
"Self-Referential Relationships 174\n",
|
||
"Advanced Many-to-Many Relationships 174\n",
|
||
"Followers on the Profile Page 178\n",
|
||
"Querying Followed Posts Using a Database Join 181\n",
|
||
"Showing Followed Posts on the Home Page 183\n",
|
||
"13. User Comments. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189\n",
|
||
"Database Representation of Comments 189\n",
|
||
"Comment Submission and Display 191\n",
|
||
"Comment Moderation 193\n",
|
||
"14. Application Programming Interfaces. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199\n",
|
||
"Introduction to REST 199\n",
|
||
"viii \n",
|
||
"| \n",
|
||
"Table of Contents\n",
|
||
"\n",
|
||
"Resources Are Everything 200\n",
|
||
"Request Methods 201\n",
|
||
"Request and Response Bodies 201\n",
|
||
"Versioning 202\n",
|
||
"RESTful Web Services with Flask 203\n",
|
||
"Creating an API Blueprint 203\n",
|
||
"Error Handling 204\n",
|
||
"User Authentication with Flask-HTTPAuth 206\n",
|
||
"Token-Based Authentication 208\n",
|
||
"Serializing Resources to and from JSON 210\n",
|
||
"Implementing Resource Endpoints 213\n",
|
||
"Pagination of Large Resource Collections 216\n",
|
||
"Testing Web Services with HTTPie 217\n",
|
||
"Part III. \n",
|
||
"The Last Mile\n",
|
||
"15. Testing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221\n",
|
||
"Obtaining Code Coverage Reports 221\n",
|
||
"The Flask Test Client 224\n",
|
||
"Testing Web Applications 225\n",
|
||
"Testing Web Services 228\n",
|
||
"End-to-End Testing with Selenium 230\n",
|
||
"Is It Worth It? 234\n",
|
||
"16. Performance. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237\n",
|
||
"Logging Slow Database Performance 237\n",
|
||
"Source Code Profiling 239\n",
|
||
"17. Deployment. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241\n",
|
||
"Deployment Workflow 241\n",
|
||
"Logging of Errors During Production 242\n",
|
||
"Cloud Deployment 243\n",
|
||
"The Heroku Platform 244\n",
|
||
"Preparing the Application 244\n",
|
||
"Testing with Heroku Local 253\n",
|
||
"Deploying with git push 254\n",
|
||
"Deploying an Upgrade 255\n",
|
||
"Docker Containers 256\n",
|
||
"Installing Docker 256\n",
|
||
"Building a Container Image 257\n",
|
||
"Running a Container 261\n",
|
||
"Table of Contents \n",
|
||
"| \n",
|
||
"ix\n",
|
||
"\n",
|
||
"Inspecting a Running Container 262\n",
|
||
"Pushing Your Container Image to an External Registry 263\n",
|
||
"Using an External Database 264\n",
|
||
"Container Orchestration with Docker Compose 265\n",
|
||
"Cleaning Up Old Containers and Images 269\n",
|
||
"Using Docker in Production 270\n",
|
||
"Traditional Deployments 270\n",
|
||
"Server Setup 271\n",
|
||
"Importing Environment Variables 271\n",
|
||
"Setting Up Logging 272\n",
|
||
"18. Additional Resources. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275\n",
|
||
"Using an Integrated Development Environment (IDE) 275\n",
|
||
"Finding Flask Extensions 276\n",
|
||
"Getting Help 276\n",
|
||
"Getting Involved with Flask 277\n",
|
||
"Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279\n",
|
||
"x \n",
|
||
"| \n",
|
||
"Table of Contents\n",
|
||
"\n",
|
||
"Preface\n",
|
||
"Flask stands out from other frameworks because it lets developers take the driver’s\n",
|
||
"seat and have full creative control of their applications. Maybe you have heard the\n",
|
||
"phrase “fighting the framework” before. This happens with most frameworks when\n",
|
||
"you decide to solve a problem with a solution that isn’t the official one. It could be\n",
|
||
"that you want to use a different database engine, or maybe a different method of\n",
|
||
"authenticating users. Deviating from the path set by the framework’s developers will\n",
|
||
"give you lots of headaches.\n",
|
||
"Flask is not like that. Do you like relational databases? Great. Flask supports them all.\n",
|
||
"Maybe you prefer a NoSQL database? No problem at all. Flask works with them too.\n",
|
||
"Want to use your own homegrown database engine? Don’t need a database at all? Still\n",
|
||
"fine. With Flask you can choose the components of your application, or even write\n",
|
||
"your own if that’s what you want. No questions asked!\n",
|
||
"The key to this freedom is that Flask was designed from the start to be extended. It\n",
|
||
"comes with a robust core that includes the basic functionality that all web applica‐\n",
|
||
"tions need and expects the rest to be provided by some of the many third-party exten‐\n",
|
||
"sions in the ecosystem—and, of course, by you.\n",
|
||
"In this book I present my workflow for developing web applications with Flask. I\n",
|
||
"don’t claim this to be the only true way to build applications with this framework. You\n",
|
||
"should take my choices as recommendations and not as gospel.\n",
|
||
"Most software development books provide small and focused code examples that\n",
|
||
"demonstrate the different features of the target technology in isolation, leaving the\n",
|
||
"“glue” code that is necessary to transform these different features into a fully working\n",
|
||
"application to be figured out by the reader. I take a completely different approach. All\n",
|
||
"the examples I present are part of a single application that starts out very simple and\n",
|
||
"is expanded in each successive chapter. This application begins life with just a few\n",
|
||
"lines of code and ends as a nicely featured blogging and social networking applica‐\n",
|
||
"tion.\n",
|
||
"xi\n",
|
||
"\n",
|
||
"Who This Book Is For\n",
|
||
"You should have some level of Python coding experience to make the most of this\n",
|
||
"book. Although the book assumes no previous Flask knowledge, Python concepts\n",
|
||
"such as packages, modules, functions, decorators, and object-oriented programming\n",
|
||
"are assumed to be well understood. Some familiarity with exceptions and diagnosing\n",
|
||
"issues from stack traces will be very useful.\n",
|
||
"While working through the examples in this book, you will spend a great deal of time\n",
|
||
"in the command line. You should feel comfortable using the command line of your\n",
|
||
"operating system.\n",
|
||
"Modern web applications cannot avoid the use of HTML, CSS, and JavaScript. The\n",
|
||
"example application that is developed throughout the book obviously makes use of\n",
|
||
"these, but the book itself does not go into a lot of detail regarding these technologies\n",
|
||
"and how they are used. Some degree of familiarity with these languages is recom‐\n",
|
||
"mended if you intend to develop complete applications without the help of a devel‐\n",
|
||
"oper versed in client-side techniques.\n",
|
||
"I released the companion application to this book as open source on GitHub.\n",
|
||
"Although GitHub makes it possible to download applications as regular ZIP or TAR\n",
|
||
"files, I strongly recommend that you install a Git client and familiarize yourself with\n",
|
||
"source code version control (at least with the basic commands to clone and check out\n",
|
||
"the different versions of the application directly from the repository). The short list of\n",
|
||
"commands that you’ll need is shown in “How to Work with the Example Code” on\n",
|
||
"page xiii. You will want to use version control for your own projects as well, so use\n",
|
||
"this book as an excuse to learn Git!\n",
|
||
"Finally, this book is not a complete and exhaustive reference on the Flask framework.\n",
|
||
"Most features are covered, but you should complement this book with the official\n",
|
||
"Flask documentation.\n",
|
||
"How This Book Is Organized\n",
|
||
"This book is divided into three parts.\n",
|
||
"Part I, Introduction to Flask, explores the basics of web application development with\n",
|
||
"the Flask framework and some of its extensions:\n",
|
||
"• Chapter 1 describes the installation and setup of the Flask framework.\n",
|
||
"• Chapter 2 dives straight into Flask with a basic application.\n",
|
||
"• Chapter 3 introduces the use of templates in Flask applications.\n",
|
||
"• Chapter 4 introduces web forms.\n",
|
||
"• Chapter 5 introduces databases.\n",
|
||
"xii \n",
|
||
"| \n",
|
||
"Preface\n",
|
||
"\n",
|
||
"• Chapter 6 introduces email support.\n",
|
||
"• Chapter 7 presents an application structure that is appropriate for medium and\n",
|
||
"large applications.\n",
|
||
"Part II, Example: A Social Blogging Application, builds Flasky, the open source blog‐\n",
|
||
"ging and social networking application that I developed for this book:\n",
|
||
"• Chapter 8 implements a user authentication system.\n",
|
||
"• Chapter 9 implements user roles and permissions.\n",
|
||
"• Chapter 10 implements user profile pages.\n",
|
||
"• Chapter 11 creates the blogging interface.\n",
|
||
"• Chapter 12 implements followers.\n",
|
||
"• Chapter 13 implements user comments for blog posts.\n",
|
||
"• Chapter 14 implements an application programming interface (API).\n",
|
||
"Part III, The Last Mile, describes some important tasks not directly related to applica‐\n",
|
||
"tion coding that need to be considered before publishing an application:\n",
|
||
"• Chapter 15 describes different unit testing strategies in detail.\n",
|
||
"• Chapter 16 gives an overview of performance analysis techniques.\n",
|
||
"• Chapter 17 describes deployment options for Flask applications, including tradi‐\n",
|
||
"tional, cloud-based, and container-based solutions.\n",
|
||
"• Chapter 18 lists additional resources.\n",
|
||
"How to Work with the Example Code\n",
|
||
"The code examples presented in this book are available for download at https://\n",
|
||
"github.com/miguelgrinberg/flasky.\n",
|
||
"The commit history in this repository was carefully created to match the order in\n",
|
||
"which concepts are presented in the book. The recommended way to work with the\n",
|
||
"code is to check out the commits starting from the oldest, then move forward\n",
|
||
"through the commit list as you make progress with the book. As an alternative, Git‐\n",
|
||
"Hub will also let you download each commit as a ZIP or TAR file.\n",
|
||
"If you decide to use Git to work with the source code, then you need to install the Git\n",
|
||
"client, which you can download from http://git-scm.com. The following command\n",
|
||
"downloads the example code using Git:\n",
|
||
"$ git clone https://github.com/miguelgrinberg/flasky.git\n",
|
||
"Preface \n",
|
||
"| \n",
|
||
"xiii\n",
|
||
"\n",
|
||
"The git clone command installs the source code from GitHub into a flasky2 folder\n",
|
||
"that is created in the current directory. This folder does not contain just source code;\n",
|
||
"a copy of the Git repository with the entire history of changes made to the application\n",
|
||
"is also included.\n",
|
||
"In the first chapter you will be asked to check out the initial release of the application,\n",
|
||
"and then, at the proper places, you will be instructed to move forward in the history.\n",
|
||
"The Git command that lets you move through the change history is git checkout.\n",
|
||
"Here is an example:\n",
|
||
"$ git checkout 1a\n",
|
||
"The 1a referenced in the command is a tag: a named point in the commit history of\n",
|
||
"the project. This repository is tagged according to the chapters of the book, so the 1a\n",
|
||
"tag used in the example sets the application files to the initial version used in Chap‐\n",
|
||
"ter 1. Most chapters have more than one tag associated with them, so, for example,\n",
|
||
"tags 5a, 5b, and so on are incremental versions presented in Chapter 5.\n",
|
||
"When you run a git checkout command as just shown, Git will display a warning\n",
|
||
"message that informs you that you are in a “detached HEAD” state. This means that\n",
|
||
"you are not in any specific code branch that can accept new commits, but instead are\n",
|
||
"looking at a particular commit in the middle of the change history of the project.\n",
|
||
"There is no reason to be alarmed by this message, but you should keep in mind that if\n",
|
||
"you make modifications to any files while in this state, issuing another git checkout\n",
|
||
"is going to fail, because Git will not know what to do with the changes you’ve made.\n",
|
||
"So, to be able to continue working with the project you will need to revert the files\n",
|
||
"that you changed back to their original state. The easiest way to do this is with the git\n",
|
||
"reset command:\n",
|
||
"$ git reset --hard\n",
|
||
"This command will destroy any local changes you have made, so you should save\n",
|
||
"anything you don’t want to lose before you use this command.\n",
|
||
"As well as checking out the source files for a version of the application, at certain\n",
|
||
"points you will need to perform additional setup tasks. For example, in some cases\n",
|
||
"you will need to install new Python packages, or apply updates to the database. You\n",
|
||
"will be told when these are necessary.\n",
|
||
"From time to time, you may want to refresh your local repository from the one on\n",
|
||
"GitHub, where bug fixes and improvements may have been applied. The commands\n",
|
||
"that achieve this are:\n",
|
||
"$ git fetch --all\n",
|
||
"$ git fetch --tags\n",
|
||
"$ git reset --hard origin/master\n",
|
||
"xiv \n",
|
||
"| \n",
|
||
"Preface\n",
|
||
"\n",
|
||
"The git fetch commands are used to update the commit history and the tags in\n",
|
||
"your local repository from the remote one on GitHub, but none of this affects the\n",
|
||
"actual source files, which are updated with the git reset command that follows.\n",
|
||
"Once again, be aware that any time git reset is used you will lose any local changes\n",
|
||
"you have made.\n",
|
||
"Another useful operation is to view all the differences between two versions of the\n",
|
||
"application. This can be very useful to understand a change in detail. From the com‐\n",
|
||
"mand line, the git diff command can do this. For example, to see the difference\n",
|
||
"between revisions 2a and 2b, use:\n",
|
||
"$ git diff 2a 2b\n",
|
||
"The differences are shown as a patch, which is not a very intuitive format to review\n",
|
||
"changes if you are not used to working with patch files. You may find that the graphi‐\n",
|
||
"cal comparisons shown by GitHub are much easier to read. For example, the differ‐\n",
|
||
"ences between revisions 2a and 2b can be viewed on GitHub at https://github.com/\n",
|
||
"miguelgrinberg/flasky/compare/2a...2b.\n",
|
||
"Using Code Examples\n",
|
||
"This book is here to help you get your job done. In general, if example code is offered\n",
|
||
"with this book, you may use it in your programs and documentation. You do not\n",
|
||
"need to contact us for permission unless you’re reproducing a significant portion of\n",
|
||
"the code. For example, writing a program that uses several chunks of code from this\n",
|
||
"book does not require permission. Selling or distributing a CD-ROM of examples\n",
|
||
"from O’Reilly books does require permission. Answering a question by citing this\n",
|
||
"book and quoting example code does not require permission. Incorporating a signifi‐\n",
|
||
"cant amount of example code from this book into your product’s documentation does\n",
|
||
"require permission.\n",
|
||
"We appreciate, but do not require, attribution. An attribution usually includes the\n",
|
||
"title, author, publisher, and ISBN. For example: “Flask Web Development, 2nd Edition,\n",
|
||
"by \n",
|
||
"Miguel \n",
|
||
"Grinberg \n",
|
||
"(O’Reilly). \n",
|
||
"Copyright \n",
|
||
"2018 \n",
|
||
"Miguel \n",
|
||
"Grinberg,\n",
|
||
"978-1-491-99173-2.”\n",
|
||
"If you feel your use of code examples falls outside fair use or the permission given\n",
|
||
"above, feel free to contact us at permissions@oreilly.com.\n",
|
||
"Conventions Used in This Book\n",
|
||
"The following typographical conventions are used in this book:\n",
|
||
"Italic\n",
|
||
"Indicates new terms, URLs, email addresses, filenames, and file extensions.\n",
|
||
"Preface \n",
|
||
"| \n",
|
||
"xv\n",
|
||
"\n",
|
||
"Constant width\n",
|
||
"Used for command-line output and program listings, as well as within para‐\n",
|
||
"graphs to refer to commands and to program elements such as variable or func‐\n",
|
||
"tion names, databases, data types, environment variables, statements, and\n",
|
||
"keywords.\n",
|
||
"Constant width bold\n",
|
||
"Shows commands or other text that should be typed literally by the user.\n",
|
||
"Constant width italic or angle brackets (<>)\n",
|
||
"Indicates text that should be replaced with user-supplied values or by values\n",
|
||
"determined by context.\n",
|
||
"This element signifies a tip or suggestion.\n",
|
||
"This element signifies a general note.\n",
|
||
"This element indicates a warning or caution.\n",
|
||
"O’Reilly Safari\n",
|
||
"Safari (formerly Safari Books Online) is a membership-based\n",
|
||
"training and reference platform for enterprise, government,\n",
|
||
"educators, and individuals.\n",
|
||
"Members have access to thousands of books, training videos, Learning Paths, interac‐\n",
|
||
"tive tutorials, and curated playlists from over 250 publishers, including O’Reilly\n",
|
||
"Media, Harvard Business Review, Prentice Hall Professional, Addison-Wesley Profes‐\n",
|
||
"sional, Microsoft Press, Sams, Que, Peachpit Press, Adobe, Focal Press, Cisco Press,\n",
|
||
"John Wiley & Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe\n",
|
||
"Press, FT Press, Apress, Manning, New Riders, McGraw-Hill, Jones & Bartlett, and\n",
|
||
"Course Technology, among others.\n",
|
||
"xvi \n",
|
||
"| \n",
|
||
"Preface\n",
|
||
"\n",
|
||
"For more information, please visit http://oreilly.com/safari.\n",
|
||
"How to Contact Us\n",
|
||
"Please address comments and questions concerning this book to the publisher:\n",
|
||
"O’Reilly Media, Inc.\n",
|
||
"1005 Gravenstein Highway North\n",
|
||
"Sebastopol, CA 95472\n",
|
||
"800-998-9938 (in the United States or Canada)\n",
|
||
"707-829-0515 (international or local)\n",
|
||
"707-829-0104 (fax)\n",
|
||
"We have a web page for this book, where we list errata, examples, and any additional\n",
|
||
"information. You can access this page at http://bit.ly/flask-web-dev2.\n",
|
||
"To comment or ask technical questions about this book, send email to bookques‐\n",
|
||
"tions@oreilly.com.\n",
|
||
"For more information about our books, courses, conferences, and news, see our web‐\n",
|
||
"site at http://www.oreilly.com.\n",
|
||
"Find us on Facebook: http://facebook.com/oreilly\n",
|
||
"Follow us on Twitter: http://twitter.com/oreillymedia\n",
|
||
"Watch us on YouTube: http://www.youtube.com/oreillymedia\n",
|
||
"Acknowledgments\n",
|
||
"I could not have written this book alone. I have received a lot of help from family, co-\n",
|
||
"workers, old friends, and new friends I’ve made along the way.\n",
|
||
"I’d like to thank Brendan Kohler for his detailed technical review and for his help in\n",
|
||
"giving shape to the chapter on application programming interfaces. I’m also in debt to\n",
|
||
"David Baumgold, Todd Brunhoff, Cecil Rock, and Matthew Hugues, who reviewed\n",
|
||
"the manuscript at different stages of completion and gave me very useful advice\n",
|
||
"regarding what to cover and how to organize the material.\n",
|
||
"Writing the code examples for this book was a considerable effort. I appreciate the\n",
|
||
"help of Daniel Hofmann, who did a thorough code review of the application and\n",
|
||
"pointed out several improvements. I’m also thankful to my teenage son, Dylan Grin‐\n",
|
||
"berg, who suspended his Minecraft addiction for a few weekends and helped me test\n",
|
||
"the code under several platforms.\n",
|
||
"O’Reilly has a wonderful program called Early Release that allows impatient readers\n",
|
||
"to have access to books while they are being written. Some of my Early Release read‐\n",
|
||
"Preface \n",
|
||
"| \n",
|
||
"xvii\n",
|
||
"\n",
|
||
"ers went the extra mile and engaged in useful conversations regarding their experi‐\n",
|
||
"ence working through the book, leading to significant improvements. I’d like to\n",
|
||
"acknowledge Sundeep Gupta, Dan Caron, Brian Wisti, and Cody Scott in particular\n",
|
||
"for the contributions they’ve made to this book.\n",
|
||
"The staff at O’Reilly Media have always been there for me. Above all I’d like to recog‐\n",
|
||
"nize my wonderful editor, Meghan Blanchette, for her support, advice, and assistance\n",
|
||
"from the very first day we met. Meg made the experience of writing my first book a\n",
|
||
"memorable one.\n",
|
||
"To conclude, I would like to give a big thank you to the awesome Flask community.\n",
|
||
"Additional Thanks for the Second Edition\n",
|
||
"I’d like to thank Ally MacDonald, my editor for the second edition of this book, and\n",
|
||
"also Susan Conant, Rachel Roumeliotis, and the whole team at O’Reilly Media for\n",
|
||
"their continued support.\n",
|
||
"The technical reviewers for this edition did a wonderful job pointing out areas to\n",
|
||
"improve and providing me with new perspectives. I’d like to recognize Lorena Mesa,\n",
|
||
"Diane Chen, and Jesse Smith for their great contributions through their feedback and\n",
|
||
"suggestions. I also greatly appreciate the help of my son, Dylan Grinberg, who pains‐\n",
|
||
"takingly tested all the code examples.\n",
|
||
"xviii \n",
|
||
"| \n",
|
||
"Preface\n",
|
||
"\n",
|
||
"PART I\n",
|
||
"Introduction to Flask\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 1\n",
|
||
"Installation\n",
|
||
"Flask is a small framework by most standards—small enough to be called a “micro-\n",
|
||
"framework,” and small enough that once you become familiar with it, you will likely\n",
|
||
"be able to read and understand all of its source code.\n",
|
||
"But being small does not mean that it does less than other frameworks. Flask was\n",
|
||
"designed as an extensible framework from the ground up; it provides a solid core\n",
|
||
"with the basic services, while extensions provide the rest. Because you can pick and\n",
|
||
"choose the extension packages that you want, you end up with a lean stack that has\n",
|
||
"no bloat and does exactly what you need.\n",
|
||
"Flask has three main dependencies. The routing, debugging, and Web Server Gateway\n",
|
||
"Interface (WSGI) subsystems come from Werkzeug; the template support is provided\n",
|
||
"by Jinja2; and the command-line integration comes from Click. These dependencies\n",
|
||
"are all authored by Armin Ronacher, the author of Flask.\n",
|
||
"Flask has no native support for accessing databases, validating web forms, authenti‐\n",
|
||
"cating users, or other high-level tasks. These and many other key services most web\n",
|
||
"applications need are available through extensions that integrate with the core pack‐\n",
|
||
"ages. As a developer, you have the power to cherry-pick the extensions that work best\n",
|
||
"for your project, or even write your own if you feel inclined to. This is in contrast\n",
|
||
"with a larger framework, where most choices have been made for you and are hard or\n",
|
||
"sometimes impossible to change.\n",
|
||
"In this chapter, you will learn how to install Flask. The only requirement is a com‐\n",
|
||
"puter with Python installed.\n",
|
||
"1\n",
|
||
"\n",
|
||
"The code examples in this book have been verified to work with\n",
|
||
"Python 3.5 and 3.6. Python 2.7 can also be used if desired, but\n",
|
||
"given that this version of Python is not going to be maintained\n",
|
||
"after the year 2020, it is strongly recommended that you use the 3.x\n",
|
||
"versions.\n",
|
||
"If you plan to use a Microsoft Windows computer to work with the\n",
|
||
"example code, you have to decide if you want to use a “native”\n",
|
||
"approach based on Windows tools, or set up your computer in a\n",
|
||
"way that allows you to adopt the more mainstream Unix-based\n",
|
||
"toolset. The code presented in this book is largely compatible with\n",
|
||
"both approaches. In the few cases where the approaches differ, the\n",
|
||
"Unix solution is followed, and alternatives for Windows are noted.\n",
|
||
"If you decide to follow a Unix workflow, you have a few options. If\n",
|
||
"you are using Windows 10, you can enable the Windows Subsys‐\n",
|
||
"tem for Linux (WSL), which is an officially supported feature that\n",
|
||
"creates an Ubuntu Linux installation that runs alongside the native\n",
|
||
"Windows interface, giving you access to a bash shell and the com‐\n",
|
||
"plete set of Unix-based tools. If WSL is not available on your sys‐\n",
|
||
"tem, another good option is Cygwin, an open-source project that\n",
|
||
"emulates the POSIX subsystem used by Unix and provides ports of\n",
|
||
"a large number of Unix tools.\n",
|
||
"Creating the Application Directory\n",
|
||
"To begin, you need to create the directory that will host the example code, which is\n",
|
||
"available in a GitHub repository. As discussed in “How to Work with the Example\n",
|
||
"Code” on page xiii, the most convenient way to do this is by checking out the code\n",
|
||
"directly from GitHub using a Git client. The following commands download the\n",
|
||
"example code from GitHub and initialize the application to version 1a, which is the\n",
|
||
"initial version you will work with:\n",
|
||
"$ git clone https://github.com/miguelgrinberg/flasky.git\n",
|
||
"$ cd flasky\n",
|
||
"$ git checkout 1a\n",
|
||
"If you prefer not to use Git and instead manually type or copy the code, you can sim‐\n",
|
||
"ply create an empty application directory as follows:\n",
|
||
"$ mkdir flasky\n",
|
||
"$ cd flasky\n",
|
||
"Virtual Environments\n",
|
||
"Now that you have the application directory created, it is time to install Flask. The\n",
|
||
"most convenient way to do that is to use a virtual environment. A virtual environment\n",
|
||
"2 \n",
|
||
"| \n",
|
||
"Chapter 1: Installation\n",
|
||
"\n",
|
||
"is a copy of the Python interpreter into which you can install packages privately,\n",
|
||
"without affecting the global Python interpreter installed in your system.\n",
|
||
"Virtual environments are very useful because they prevent package clutter and ver‐\n",
|
||
"sion conflicts in the system’s Python interpreter. Creating a virtual environment for\n",
|
||
"each project ensures that applications have access only to the packages that they use,\n",
|
||
"while the global interpreter remains neat and clean and serves only as a source from\n",
|
||
"which more virtual environments can be created. As an added benefit, unlike the\n",
|
||
"system-wide Python interpreter, virtual environments can be created and managed\n",
|
||
"without administrator rights.\n",
|
||
"Creating a Virtual Environment with Python 3\n",
|
||
"The creation of virtual environments is an area where Python 3 and Python 2 inter‐\n",
|
||
"preters differ. With Python 3, virtual environments are supported natively by the\n",
|
||
"venv package that is part of the Python standard library.\n",
|
||
"If you are using the stock Python 3 interpreter on an Ubuntu Linux\n",
|
||
"system, the standard venv package is not installed by default. To\n",
|
||
"add it to your system, install the python3-venv package as follows:\n",
|
||
"$ sudo apt-get install python3-venv\n",
|
||
"The command that creates a virtual environment has the following structure:\n",
|
||
"$ python3 -m venv virtual-environment-name\n",
|
||
"The -m venv option runs the venv package from the standard library as a standalone\n",
|
||
"script, passing the desired name as an argument.\n",
|
||
"You are now going to create a virtual environment inside the flasky directory. A com‐\n",
|
||
"monly used convention for virtual environments is to call them venv, but you can use\n",
|
||
"a different name if you prefer. Make sure your current directory is set to flasky, and\n",
|
||
"then run this command:\n",
|
||
"$ python3 -m venv venv\n",
|
||
"After the command completes, you will have a subdirectory with the name venv\n",
|
||
"inside flasky, with a brand-new virtual environment that contains a Python inter‐\n",
|
||
"preter for exclusive use by this project.\n",
|
||
"Creating a Virtual Environment with Python 2\n",
|
||
"Python 2 does not have a venv package. In this version of the Python interpreter, vir‐\n",
|
||
"tual environments are created with the third-party utility virtualenv.\n",
|
||
"Creating a Virtual Environment with Python 3 \n",
|
||
"| \n",
|
||
"3\n",
|
||
"\n",
|
||
"Make sure your current directory is set to flasky, and then use one of the following\n",
|
||
"two commands, depending on your operating system. If you are using Linux or\n",
|
||
"macOS, the command is:\n",
|
||
"$ sudo pip install virtualenv\n",
|
||
"If you are using Microsoft Windows, make sure you open a command prompt win‐\n",
|
||
"dow using the “Run as Administrator” option, and then run this command:\n",
|
||
"$ pip install virtualenv\n",
|
||
"The virtualenv command takes the name of the virtual environment as its argu‐\n",
|
||
"ment. Make sure your current directory is set to flasky, and then run the following\n",
|
||
"command to create a virtual environment called venv:\n",
|
||
"$ virtualenv venv\n",
|
||
"New python executable in venv/bin/python2.7\n",
|
||
"Also creating executable in venv/bin/python\n",
|
||
"Installing setuptools, pip, wheel...done.\n",
|
||
"A subdirectory with the venv name will be created in the current directory, and all\n",
|
||
"files associated with the virtual environment will be inside it.\n",
|
||
"Working with a Virtual Environment\n",
|
||
"When you want to start using a virtual environment, you have to “activate” it. If you\n",
|
||
"are using a Linux or macOS computer, you can activate the virtual environment with\n",
|
||
"this command:\n",
|
||
"$ source venv/bin/activate\n",
|
||
"If you are using Microsoft Windows, the activation command is:\n",
|
||
"$ venv\\Scripts\\activate\n",
|
||
"When a virtual environment is activated, the location of its Python interpreter is\n",
|
||
"added to the PATH environment variable in your current command session, which\n",
|
||
"determines where to look for executable files. To remind you that you have activated\n",
|
||
"a virtual environment, the activation command modifies your command prompt to\n",
|
||
"include the name of the environment:\n",
|
||
"(venv) $\n",
|
||
"After a virtual environment is activated, typing python at the command prompt will\n",
|
||
"invoke the interpreter from the virtual environment instead of the system-wide inter‐\n",
|
||
"preter. If you are using more than one command prompt window, you have to acti‐\n",
|
||
"vate the virtual environment in each of them.\n",
|
||
"4 \n",
|
||
"| \n",
|
||
"Chapter 1: Installation\n",
|
||
"\n",
|
||
"While activating a virtual environment is usually the most conve‐\n",
|
||
"nient option, you can also use a virtual environment without acti‐\n",
|
||
"vating it. For example, you can start a Python console for the venv\n",
|
||
"virtual environment by running venv/bin/python on Linux or\n",
|
||
"macOS, or venv\\Scripts\\python on Microsoft Windows.\n",
|
||
"When you are done working with the virtual environment, type deactivate at the\n",
|
||
"command prompt to restore the PATH environment variable for your terminal session\n",
|
||
"and the command prompt to their original states.\n",
|
||
"Installing Python Packages with pip\n",
|
||
"Python packages are installed with the pip package manager, which is included in all\n",
|
||
"virtual environments. Like the python command, typing pip in a command prompt\n",
|
||
"session will invoke the version of this tool that belongs to the activated virtual envi‐\n",
|
||
"ronment.\n",
|
||
"To install Flask into the virtual environment, make sure the venv virtual environment\n",
|
||
"is activated, and then run the following command:\n",
|
||
"(venv) $ pip install flask\n",
|
||
"When you execute this command, pip will not only install Flask, but also all of its\n",
|
||
"dependencies. You can check what packages are installed in the virtual environment\n",
|
||
"at any time using the pip freeze command:\n",
|
||
"(venv) $ pip freeze\n",
|
||
"click==6.7\n",
|
||
"Flask==0.12.2\n",
|
||
"itsdangerous==0.24\n",
|
||
"Jinja2==2.9.6\n",
|
||
"MarkupSafe==1.0\n",
|
||
"Werkzeug==0.12.2\n",
|
||
"The output of pip freeze includes detailed version numbers for each installed pack‐\n",
|
||
"age. The version numbers that you get are likely going to be different from the ones\n",
|
||
"shown here.\n",
|
||
"You can also verify that Flask was correctly installed by starting the Python inter‐\n",
|
||
"preter and trying to import it:\n",
|
||
"(venv) $ python\n",
|
||
">>> import flask\n",
|
||
">>>\n",
|
||
"If no errors appear, you can congratulate yourself: you are ready for the next chapter,\n",
|
||
"where you will write your first web application.\n",
|
||
"Installing Python Packages with pip \n",
|
||
"| \n",
|
||
"5\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 2\n",
|
||
"Basic Application Structure\n",
|
||
"In this chapter, you will learn about the different parts of a Flask application. You will\n",
|
||
"also write and run your first Flask web application.\n",
|
||
"Initialization\n",
|
||
"All Flask applications must create an application instance. The web server passes all\n",
|
||
"requests it receives from clients to this object for handling, using a protocol called\n",
|
||
"Web Server Gateway Interface (WSGI, pronounced “wiz-ghee”). The application\n",
|
||
"instance is an object of class Flask, usually created as follows:\n",
|
||
"from flask import Flask\n",
|
||
"app = Flask(__name__)\n",
|
||
"The only required argument to the Flask class constructor is the name of the main\n",
|
||
"module or package of the application. For most applications, Python’s __name__ vari‐\n",
|
||
"able is the correct value for this argument.\n",
|
||
"The __name__ argument that is passed to the Flask application con‐\n",
|
||
"structor is a source of confusion among new Flask developers.\n",
|
||
"Flask uses this argument to determine the location of the applica‐\n",
|
||
"tion, which in turn allows it to locate other files that are part of the\n",
|
||
"application, such as images and templates.\n",
|
||
"Later you will learn more complex ways to initialize an application, but for simple\n",
|
||
"applications this is all that is needed.\n",
|
||
"7\n",
|
||
"\n",
|
||
"Routes and View Functions\n",
|
||
"Clients such as web browsers send requests to the web server, which in turn sends\n",
|
||
"them to the Flask application instance. The Flask application instance needs to know\n",
|
||
"what code it needs to run for each URL requested, so it keeps a mapping of URLs to\n",
|
||
"Python functions. The association between a URL and the function that handles it is\n",
|
||
"called a route.\n",
|
||
"The most convenient way to define a route in a Flask application is through the\n",
|
||
"app.route decorator exposed by the application instance. The following example\n",
|
||
"shows how a route is declared using this decorator:\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" return '<h1>Hello World!</h1>'\n",
|
||
"Decorators are a standard feature of the Python language. A com‐\n",
|
||
"mon use of decorators is to register functions as handler functions\n",
|
||
"to be invoked when certain events occur.\n",
|
||
"The previous example registers function index() as the handler for the application’s\n",
|
||
"root URL. While the app.route decorator is the preferred method to register view\n",
|
||
"functions, Flask also offers a more traditional way to set up the application routes\n",
|
||
"with the app.add_url_rule() method, which in its most basic form takes three argu‐\n",
|
||
"ments: the URL, the endpoint name, and the view function. The following example\n",
|
||
"uses app.add_url_rule() to register an index() function that is equivalent to the\n",
|
||
"one shown previously:\n",
|
||
"def index():\n",
|
||
" return '<h1>Hello World!</h1>'\n",
|
||
"app.add_url_rule('/', 'index', index)\n",
|
||
"Functions like index() that handle application URLs are called view functions. If the\n",
|
||
"application is deployed on a server associated with the www.example.com domain\n",
|
||
"name, then navigating to http://www.example.com/ in your browser would trigger\n",
|
||
"index() to run on the server. The return value of this view function is the response\n",
|
||
"the client receives. If the client is a web browser, this response is the document that is\n",
|
||
"displayed to the user in the browser window. A response returned by a view function\n",
|
||
"can be a simple string with HTML content, but it can also take more complex forms,\n",
|
||
"as you will see later.\n",
|
||
"8 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"Embedding response strings with HTML code in Python source\n",
|
||
"files leads to code that is difficult to maintain. The examples in this\n",
|
||
"chapter do it only to introduce the concept of responses. You will\n",
|
||
"learn a better way to generate HTML responses in Chapter 3.\n",
|
||
"If you pay attention to how some URLs for services that you use every day are\n",
|
||
"formed, you will notice that many have variable sections. For example, the URL for\n",
|
||
"your Facebook profile page has the format https://www.facebook.com/<your-name>,\n",
|
||
"which includes your username, making it different for each user. Flask supports these\n",
|
||
"types of URLs using a special syntax in the app.route decorator. The following exam‐\n",
|
||
"ple defines a route that has a dynamic component:\n",
|
||
"@app.route('/user/<name>')\n",
|
||
"def user(name):\n",
|
||
" return '<h1>Hello, {}!</h1>'.format(name)\n",
|
||
"The portion of the route URL enclosed in angle brackets is the dynamic part. Any\n",
|
||
"URLs that match the static portions will be mapped to this route, and when the view\n",
|
||
"function is invoked, the dynamic component will be passed as an argument. In the\n",
|
||
"preceding example, the name argument is used to generate a response that includes a\n",
|
||
"personalized greeting.\n",
|
||
"The dynamic components in routes are strings by default but can also be of different\n",
|
||
"types. For example, the route /user/<int:id> would match only URLs that have an\n",
|
||
"integer in the id dynamic segment, such as /user/123. Flask supports the types\n",
|
||
"string, int, float, and path for routes. The path type is a special string type that can\n",
|
||
"include forward slashes, unlike the string type.\n",
|
||
"A Complete Application\n",
|
||
"In the previous sections you learned about the different parts of a Flask web applica‐\n",
|
||
"tion, and now it is time to write your first one. The hello.py application script shown\n",
|
||
"in Example 2-1 defines an application instance and a single route and view function,\n",
|
||
"as described earlier.\n",
|
||
"Example 2-1. hello.py: A complete Flask application\n",
|
||
"from flask import Flask\n",
|
||
"app = Flask(__name__)\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" return '<h1>Hello World!</h1>'\n",
|
||
"A Complete Application \n",
|
||
"| \n",
|
||
"9\n",
|
||
"\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can now run git checkout 2a to check out this version of the\n",
|
||
"application.\n",
|
||
"Development Web Server\n",
|
||
"Flask applications include a development web server that can be started with the\n",
|
||
"flask run command. This command looks for the name of the Python script that\n",
|
||
"contains the application instance in the FLASK_APP environment variable.\n",
|
||
"To start the hello.py application from the previous section, first make sure the virtual\n",
|
||
"environment you created earlier is activated and has Flask installed in it. For Linux\n",
|
||
"and macOS users, start the web server as follows:\n",
|
||
"(venv) $ export FLASK_APP=hello.py\n",
|
||
"(venv) $ flask run\n",
|
||
" * Serving Flask app \"hello\"\n",
|
||
" * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)\n",
|
||
"For Microsoft Windows users, the only difference is in how the FLASK_APP environ‐\n",
|
||
"ment variable is set:\n",
|
||
"(venv) $ set FLASK_APP=hello.py\n",
|
||
"(venv) $ flask run\n",
|
||
" * Serving Flask app \"hello\"\n",
|
||
" * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)\n",
|
||
"Once the server starts up, it goes into a loop that accepts requests and services them.\n",
|
||
"This loop continues until you stop the application by pressing Ctrl+C.\n",
|
||
"With the server running, open your web browser and type http://localhost:5000/\n",
|
||
"in the address bar. Figure 2-1 shows what you’ll see after connecting to the applica‐\n",
|
||
"tion.\n",
|
||
"10 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"Figure 2-1. hello.py Flask application\n",
|
||
"If you type anything else after the base URL, the application will not know how to\n",
|
||
"handle it and will return an error code 404 to the browser—the familiar error that\n",
|
||
"you get when you navigate to a web page that does not exist.\n",
|
||
"The web server provided by Flask is intended to be used only for\n",
|
||
"development and testing. You will learn about production web\n",
|
||
"servers in Chapter 17.\n",
|
||
"The Flask development web server can also be started program‐\n",
|
||
"matically by invoking the app.run() method. Older versions of\n",
|
||
"Flask that did not have the flask command required the server to\n",
|
||
"be started by running the application’s main script, which had to\n",
|
||
"include the following snippet at the end:\n",
|
||
"if __name__ == '__main__':\n",
|
||
" app.run()\n",
|
||
"While the flask run command makes this practice unnecessary,\n",
|
||
"the app.run() method can still be useful on certain occasions, such\n",
|
||
"as unit testing, as you will learn in Chapter 15.\n",
|
||
"Development Web Server \n",
|
||
"| \n",
|
||
"11\n",
|
||
"\n",
|
||
"Dynamic Routes\n",
|
||
"The second version of the application, shown in Example 2-2, adds a second route\n",
|
||
"that is dynamic. When you visit the dynamic URL in your browser, you are presented\n",
|
||
"with a personalized greeting that includes the name provided in the URL.\n",
|
||
"Example 2-2. hello.py: Flask application with a dynamic route\n",
|
||
"from flask import Flask\n",
|
||
"app = Flask(__name__)\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" return '<h1>Hello World!</h1>'\n",
|
||
"@app.route('/user/<name>')\n",
|
||
"def user(name):\n",
|
||
" return '<h1>Hello, {}!</h1>'.format(name)\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can now run git checkout 2b to check out this version of the\n",
|
||
"application.\n",
|
||
"To test the dynamic route, make sure the server is running and then navigate to\n",
|
||
"http://localhost:5000/user/Dave. The application will respond with the personalized\n",
|
||
"greeting using the name dynamic argument. Try using different names in the URL to\n",
|
||
"see how the view function always generates the response based on the name given.\n",
|
||
"An example is shown in Figure 2-2.\n",
|
||
"12 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"Figure 2-2. Dynamic route\n",
|
||
"Debug Mode\n",
|
||
"Flask applications can optionally be executed in debug mode. In this mode, two very\n",
|
||
"convenient modules of the development server called the reloader and the debugger\n",
|
||
"are enabled by default.\n",
|
||
"When the reloader is enabled, Flask watches all the source code files of your project\n",
|
||
"and automatically restarts the server when any of the files are modified. Having a\n",
|
||
"server running with the reloader enabled is extremely useful during development,\n",
|
||
"because every time you modify and save a source file, the server automatically restarts\n",
|
||
"and picks up the change.\n",
|
||
"The debugger is a web-based tool that appears in your browser when your application\n",
|
||
"raises an unhandled exception. The web browser window transforms into an interac‐\n",
|
||
"tive stack trace that allows you to inspect source code and evaluate expressions in any\n",
|
||
"place in the call stack. You can see how the debugger looks in Figure 2-3.\n",
|
||
"Debug Mode \n",
|
||
"| \n",
|
||
"13\n",
|
||
"\n",
|
||
"Figure 2-3. Flask debugger\n",
|
||
"By default, debug mode is disabled. To enable it, set a FLASK_DEBUG=1 environment\n",
|
||
"variable before invoking flask run:\n",
|
||
"(venv) $ export FLASK_APP=hello.py\n",
|
||
"(venv) $ export FLASK_DEBUG=1\n",
|
||
"(venv) $ flask run\n",
|
||
" * Serving Flask app \"hello\"\n",
|
||
" * Forcing debug mode on\n",
|
||
" * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)\n",
|
||
" * Restarting with stat\n",
|
||
" * Debugger is active!\n",
|
||
" * Debugger PIN: 273-181-528\n",
|
||
"If you are using Microsoft Windows, use set instead of export to set the environ‐\n",
|
||
"ment variables.\n",
|
||
"14 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"If you start your server with the app.run() method, the FLASK_APP\n",
|
||
"and FLASK_DEBUG environment variables are not used. To enable\n",
|
||
"debug mode programmatically, use app.run(debug=True).\n",
|
||
"Never enable debug mode on a production server. The debugger in\n",
|
||
"particular allows the client to request remote code execution, so it\n",
|
||
"makes your production server vulnerable to attacks. As a simple\n",
|
||
"protection measure, the debugger needs to be activated with a PIN,\n",
|
||
"printed to the console by the flask run command.\n",
|
||
"Command-Line Options\n",
|
||
"The flask command supports a number of options. To see what’s available, you can\n",
|
||
"run flask --help or just flask without any arguments:\n",
|
||
"(venv) $ flask --help\n",
|
||
"Usage: flask [OPTIONS] COMMAND [ARGS]...\n",
|
||
" This shell command acts as general utility script for Flask applications.\n",
|
||
" It loads the application configured (through the FLASK_APP environment\n",
|
||
" variable) and then provides commands either provided by the application or\n",
|
||
" Flask itself.\n",
|
||
" The most useful commands are the \"run\" and \"shell\" command.\n",
|
||
" Example usage:\n",
|
||
" $ export FLASK_APP=hello.py\n",
|
||
" $ export FLASK_DEBUG=1\n",
|
||
" $ flask run\n",
|
||
"Options:\n",
|
||
" --version Show the flask version\n",
|
||
" --help Show this message and exit.\n",
|
||
"Commands:\n",
|
||
" run Runs a development server.\n",
|
||
" shell Runs a shell in the app context.\n",
|
||
"The flask shell command is used to start a Python shell session in the context of\n",
|
||
"the application. You can use this session to run maintenance tasks or tests, or to\n",
|
||
"debug issues. Actual examples where this command is useful will be presented later,\n",
|
||
"in several chapters.\n",
|
||
"Command-Line Options \n",
|
||
"| \n",
|
||
"15\n",
|
||
"\n",
|
||
"You are already familiar with the flask run command, which, as its name implies,\n",
|
||
"runs the application with the development web server. This command has many\n",
|
||
"options:\n",
|
||
"(venv) $ flask run --help\n",
|
||
"Usage: flask run [OPTIONS]\n",
|
||
" Runs a local development server for the Flask application.\n",
|
||
" This local server is recommended for development purposes only but it can\n",
|
||
" also be used for simple intranet deployments. By default it will not\n",
|
||
" support any sort of concurrency at all to simplify debugging. This can be\n",
|
||
" changed with the --with-threads option which will enable basic\n",
|
||
" multithreading.\n",
|
||
" The reloader and debugger are by default enabled if the debug flag of\n",
|
||
" Flask is enabled and disabled otherwise.\n",
|
||
"Options:\n",
|
||
" -h, --host TEXT The interface to bind to.\n",
|
||
" -p, --port INTEGER The port to bind to.\n",
|
||
" --reload / --no-reload Enable or disable the reloader. By default\n",
|
||
" the reloader is active if debug is enabled.\n",
|
||
" --debugger / --no-debugger Enable or disable the debugger. By default\n",
|
||
" the debugger is active if debug is enabled.\n",
|
||
" --eager-loading / --lazy-loader\n",
|
||
" Enable or disable eager loading. By default\n",
|
||
" eager loading is enabled if the reloader is\n",
|
||
" disabled.\n",
|
||
" --with-threads / --without-threads\n",
|
||
" Enable or disable multithreading.\n",
|
||
" --help Show this message and exit.\n",
|
||
"The --host argument is particularly useful because it tells the web server what net‐\n",
|
||
"work interface to listen to for connections from clients. By default, Flask’s develop‐\n",
|
||
"ment web server listens for connections on localhost, so only connections originating\n",
|
||
"from the computer running the server are accepted. The following command makes\n",
|
||
"the web server listen for connections on the public network interface, enabling other\n",
|
||
"computers in the same network to connect as well:\n",
|
||
"(venv) $ flask run --host 0.0.0.0\n",
|
||
" * Serving Flask app \"hello\"\n",
|
||
" * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)\n",
|
||
"The web server should now be accessible from any computer in the network at http://\n",
|
||
"a.b.c.d:5000, where a.b.c.d is the IP address of the computer running the server in\n",
|
||
"your network.\n",
|
||
"The --reload, --no-reload, --debugger, and --no-debugger options provide a\n",
|
||
"greater degree of control on top of the debug mode setting. For example, if debug\n",
|
||
"16 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"mode is enabled, --no-debugger can be used to turn off the debugger, while keeping\n",
|
||
"debug mode and the reloader enabled.\n",
|
||
"The Request-Response Cycle\n",
|
||
"Now that you have played with a basic Flask application, you might want to know\n",
|
||
"more about how Flask works its magic. The following sections describe some of the\n",
|
||
"design aspects of the framework.\n",
|
||
"Application and Request Contexts\n",
|
||
"When Flask receives a request from a client, it needs to make a few objects available\n",
|
||
"to the view function that will handle it. A good example is the request object, which\n",
|
||
"encapsulates the HTTP request sent by the client.\n",
|
||
"The obvious way in which Flask could give a view function access to the request\n",
|
||
"object is by sending it as an argument, but that would require every single view func‐\n",
|
||
"tion in the application to have an extra argument. Things get more complicated if you\n",
|
||
"consider that the request object is not the only object that view functions might need\n",
|
||
"to access to fulfill a request.\n",
|
||
"To avoid cluttering view functions with lots of arguments that may not always be\n",
|
||
"needed, Flask uses contexts to temporarily make certain objects globally accessible.\n",
|
||
"Thanks to contexts, view functions like the following one can be written:\n",
|
||
"from flask import request\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" user_agent = request.headers.get('User-Agent')\n",
|
||
" return '<p>Your browser is {}</p>'.format(user_agent)\n",
|
||
"Note how in this view function, request is used as if it were a global variable. In real‐\n",
|
||
"ity, request cannot be a global variable; in a multithreaded server several threads can\n",
|
||
"be working on different requests from different clients all at the same time, so each\n",
|
||
"thread needs to see a different object in request. Contexts enable Flask to make cer‐\n",
|
||
"tain variables globally accessible to a thread without interfering with the other\n",
|
||
"threads.\n",
|
||
"A thread is the smallest sequence of instructions that can be man‐\n",
|
||
"aged independently. It is common for a process to have multiple\n",
|
||
"active threads, sometimes sharing resources such as memory or file\n",
|
||
"handles. Multithreaded web servers start a pool of threads and\n",
|
||
"select a thread from the pool to handle each incoming request.\n",
|
||
"The Request-Response Cycle \n",
|
||
"| \n",
|
||
"17\n",
|
||
"\n",
|
||
"There are two contexts in Flask: the application context and the request context.\n",
|
||
"Table 2-1 shows the variables exposed by each of these contexts.\n",
|
||
"Table 2-1. Flask context globals\n",
|
||
"Variable name\n",
|
||
"Context\n",
|
||
"Description\n",
|
||
"current_app Application\n",
|
||
"context\n",
|
||
"The application instance for the active application.\n",
|
||
"g\n",
|
||
"Application\n",
|
||
"context\n",
|
||
"An object that the application can use for temporary storage during the handling of a\n",
|
||
"request. This variable is reset with each request.\n",
|
||
"request\n",
|
||
"Request context\n",
|
||
"The request object, which encapsulates the contents of an HTTP request sent by the\n",
|
||
"client.\n",
|
||
"session\n",
|
||
"Request context\n",
|
||
"The user session, a dictionary that the application can use to store values that are\n",
|
||
"“remembered” between requests.\n",
|
||
"Flask activates (or pushes) the application and request contexts before dispatching a\n",
|
||
"request to the application, and removes them after the request is handled. When the\n",
|
||
"application context is pushed, the current_app and g variables become available to\n",
|
||
"the thread. Likewise, when the request context is pushed, request and session\n",
|
||
"become available as well. If any of these variables are accessed without an active appli‐\n",
|
||
"cation or request context, an error is generated. The four context variables will be\n",
|
||
"covered in detail in this and later chapters, so don’t worry if you don’t understand\n",
|
||
"why they are useful yet.\n",
|
||
"The following Python shell session demonstrates how the application context works:\n",
|
||
">>> from hello import app\n",
|
||
">>> from flask import current_app\n",
|
||
">>> current_app.name\n",
|
||
"Traceback (most recent call last):\n",
|
||
"...\n",
|
||
"RuntimeError: working outside of application context\n",
|
||
">>> app_ctx = app.app_context()\n",
|
||
">>> app_ctx.push()\n",
|
||
">>> current_app.name\n",
|
||
"'hello'\n",
|
||
">>> app_ctx.pop()\n",
|
||
"In this example, current_app.name fails when there is no application context active\n",
|
||
"but becomes valid once an application context for the application is pushed. Note\n",
|
||
"how an application context is obtained by invoking app.app_context() on the appli‐\n",
|
||
"cation instance.\n",
|
||
"Request Dispatching\n",
|
||
"When the application receives a request from a client, it needs to find out what view\n",
|
||
"function to invoke to service it. For this task, Flask looks up the URL given in the\n",
|
||
"18 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"request in the application’s URL map, which contains a mapping of URLs to the view\n",
|
||
"functions that handle them. Flask builds this map using the data provided in the\n",
|
||
"app.route decorator, or the equivalent non-decorator version, app.add_url_rule().\n",
|
||
"To see what the URL map in a Flask application looks like, you can inspect the map\n",
|
||
"created for hello.py in the Python shell. Before you try this, make sure that your vir‐\n",
|
||
"tual environment is activated:\n",
|
||
"(venv) $ python\n",
|
||
">>> from hello import app\n",
|
||
">>> app.url_map\n",
|
||
"Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>,\n",
|
||
" <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,\n",
|
||
" <Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])\n",
|
||
"The / and /user/<name> routes were defined by the app.route decorators in the\n",
|
||
"application. The /static/<filename> route is a special route added by Flask to give\n",
|
||
"access to static files. You will learn more about static files in Chapter 3.\n",
|
||
"The (HEAD, OPTIONS, GET) elements shown in the URL map are the request methods\n",
|
||
"that are handled by the routes. The HTTP specification defines that all requests are\n",
|
||
"issued with a method, which normally indicates what action the client is asking the\n",
|
||
"server to perform. Flask attaches methods to each route so that different request\n",
|
||
"methods sent to the same URL can be handled by different view functions. The HEAD\n",
|
||
"and OPTIONS methods are managed automatically by Flask, so in practice it can be\n",
|
||
"said that in this application the three routes in the URL map are attached to the GET\n",
|
||
"method, which is used when the client wants to request information such as a web\n",
|
||
"page. You will learn how to create routes for other request methods in Chapter 4.\n",
|
||
"The Request Object\n",
|
||
"You have seen that Flask exposes a request object as a context variable named\n",
|
||
"request. This is an extremely useful object that contains all the information that the\n",
|
||
"client included in the HTTP request. Table 2-2 enumerates the most commonly used\n",
|
||
"attributes and methods of the Flask request object.\n",
|
||
"Table 2-2. Flask request object\n",
|
||
"Attribute or Method\n",
|
||
"Description\n",
|
||
"form\n",
|
||
"A dictionary with all the form fields submitted with the request.\n",
|
||
"args\n",
|
||
"A dictionary with all the arguments passed in the query string of the URL.\n",
|
||
"values\n",
|
||
"A dictionary that combines the values in form and args.\n",
|
||
"cookies\n",
|
||
"A dictionary with all the cookies included in the request.\n",
|
||
"headers\n",
|
||
"A dictionary with all the HTTP headers included in the request.\n",
|
||
"files\n",
|
||
"A dictionary with all the file uploads included with the request.\n",
|
||
"The Request-Response Cycle \n",
|
||
"| \n",
|
||
"19\n",
|
||
"\n",
|
||
"Attribute or Method Description\n",
|
||
"get_data()\n",
|
||
"Returns the buffered data from the request body.\n",
|
||
"get_json()\n",
|
||
"Returns a Python dictionary with the parsed JSON included in the body of the request.\n",
|
||
"blueprint\n",
|
||
"The name of the Flask blueprint that is handling the request. Blueprints are introduced in Chapter 7.\n",
|
||
"endpoint\n",
|
||
"The name of the Flask endpoint that is handling the request. Flask uses the name of the view function\n",
|
||
"as the endpoint name for a route.\n",
|
||
"method\n",
|
||
"The HTTP request method, such as GET or POST.\n",
|
||
"scheme\n",
|
||
"The URL scheme (http or https).\n",
|
||
"is_secure()\n",
|
||
"Returns True if the request came through a secure (HTTPS) connection.\n",
|
||
"host\n",
|
||
"The host defined in the request, including the port number if given by the client.\n",
|
||
"path\n",
|
||
"The path portion of the URL.\n",
|
||
"query_string\n",
|
||
"The query string portion of the URL, as a raw binary value.\n",
|
||
"full_path\n",
|
||
"The path and query string portions of the URL.\n",
|
||
"url\n",
|
||
"The complete URL requested by the client.\n",
|
||
"base_url\n",
|
||
"Same as url, but without the query string component.\n",
|
||
"remote_addr\n",
|
||
"The IP address of the client.\n",
|
||
"environ\n",
|
||
"The raw WSGI environment dictionary for the request.\n",
|
||
"Request Hooks\n",
|
||
"Sometimes it is useful to execute code before or after each request is processed. For\n",
|
||
"example, at the start of each request it may be necessary to create a database connec‐\n",
|
||
"tion or authenticate the user making the request. Instead of duplicating the code that\n",
|
||
"performs these actions in every view function, Flask gives you the option to register\n",
|
||
"common functions to be invoked before or after a request is dispatched.\n",
|
||
"Request hooks are implemented as decorators. These are the four hooks supported by\n",
|
||
"Flask:\n",
|
||
"before_request\n",
|
||
"Registers a function to run before each request.\n",
|
||
"before_first_request\n",
|
||
"Registers a function to run only before the first request is handled. This can be a\n",
|
||
"convenient way to add server initialization tasks.\n",
|
||
"after_request\n",
|
||
"Registers a function to run after each request, but only if no unhandled excep‐\n",
|
||
"tions occurred.\n",
|
||
"teardown_request\n",
|
||
"Registers a function to run after each request, even if unhandled exceptions\n",
|
||
"occurred.\n",
|
||
"20 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"A common pattern to share data between request hook functions and view functions\n",
|
||
"is to use the g context global as storage. For example, a before_request handler can\n",
|
||
"load the logged-in user from the database and store it in g.user. Later, when the view\n",
|
||
"function is invoked, it can retrieve the user from there.\n",
|
||
"Examples of request hooks will be shown in future chapters, so don’t worry if the pur‐\n",
|
||
"pose of these hooks does not quite make sense yet.\n",
|
||
"Responses\n",
|
||
"When Flask invokes a view function, it expects its return value to be the response to\n",
|
||
"the request. In most cases the response is a simple string that is sent back to the client\n",
|
||
"as an HTML page.\n",
|
||
"But the HTTP protocol requires more than a string as a response to a request. A very\n",
|
||
"important part of the HTTP response is the status code, which Flask by default sets to\n",
|
||
"200, the code that indicates that the request was carried out successfully.\n",
|
||
"When a view function needs to respond with a different status code, it can add the\n",
|
||
"numeric code as a second return value after the response text. For example, the fol‐\n",
|
||
"lowing view function returns a 400 status code, the code for a bad request error:\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" return '<h1>Bad Request</h1>', 400\n",
|
||
"Responses returned by view functions can also take a third argument, a dictionary of\n",
|
||
"headers that are added to the HTTP response. You will see an example of custom\n",
|
||
"response headers in Chapter 14.\n",
|
||
"Instead of returning one, two, or three values as a tuple, Flask view functions have the\n",
|
||
"option of returning a response object. The make_response() function takes one, two,\n",
|
||
"or three arguments, the same values that can be returned from a view function, and\n",
|
||
"returns an equivalent response object. Sometimes it is useful to generate the response\n",
|
||
"object inside the view function, and then use its methods to further configure the\n",
|
||
"response. The following example creates a response object and then sets a cookie in it:\n",
|
||
"from flask import make_response\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" response = make_response('<h1>This document carries a cookie!</h1>')\n",
|
||
" response.set_cookie('answer', '42')\n",
|
||
" return response\n",
|
||
"Table 2-3 shows the most commonly used attributes and methods available in\n",
|
||
"response objects.\n",
|
||
"The Request-Response Cycle \n",
|
||
"| \n",
|
||
"21\n",
|
||
"\n",
|
||
"Table 2-3. Flask response object\n",
|
||
"Attribute or Method\n",
|
||
"Description\n",
|
||
"status_code\n",
|
||
"The numeric HTTP status code\n",
|
||
"headers\n",
|
||
"A dictionary-like object with all the headers that will be sent with the response\n",
|
||
"set_cookie()\n",
|
||
"Adds a cookie to the response\n",
|
||
"delete_cookie()\n",
|
||
"Removes a cookie\n",
|
||
"content_length\n",
|
||
"The length of the response body\n",
|
||
"content_type\n",
|
||
"The media type of the response body\n",
|
||
"set_data()\n",
|
||
"Sets the response body as a string or bytes value\n",
|
||
"get_data()\n",
|
||
"Gets the response body\n",
|
||
"There is a special type of response called a redirect. This response does not include a\n",
|
||
"page document; it just gives the browser a new URL to navigate to. A very common\n",
|
||
"use of redirects is when working with web forms, as you will learn in Chapter 4.\n",
|
||
"A redirect is typically indicated with a 302 response status code and the URL to go to\n",
|
||
"given in a Location header. A redirect response can be generated manually with a\n",
|
||
"three-value return or with a response object, but given its frequent use, Flask provides\n",
|
||
"a redirect() helper function that creates this type of response:\n",
|
||
"from flask import redirect\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" return redirect('http://www.example.com')\n",
|
||
"Another special response is issued with the abort() function, which is used for error\n",
|
||
"handling. The following example returns status code 404 if the id dynamic argument\n",
|
||
"given in the URL does not represent a valid user:\n",
|
||
"from flask import abort\n",
|
||
"@app.route('/user/<id>')\n",
|
||
"def get_user(id):\n",
|
||
" user = load_user(id)\n",
|
||
" if not user:\n",
|
||
" abort(404)\n",
|
||
" return '<h1>Hello, {}</h1>'.format(user.name)\n",
|
||
"Note that abort() does not return control back to the function because it raises an\n",
|
||
"exception.\n",
|
||
"22 \n",
|
||
"| \n",
|
||
"Chapter 2: Basic Application Structure\n",
|
||
"\n",
|
||
"Flask Extensions\n",
|
||
"Flask is designed to be extended. It intentionally stays out of areas of important func‐\n",
|
||
"tionality such as database and user authentication, giving you the freedom to select\n",
|
||
"the packages that fit your application the best, or to write your own if you so desire.\n",
|
||
"A great variety of Flask extensions for many different purposes have been created by\n",
|
||
"the community, and if that is not enough, any standard Python package or library can\n",
|
||
"be used as well. You will use your first Flask extension in Chapter 3.\n",
|
||
"This chapter introduced the concept of responses to requests, but there is a lot more\n",
|
||
"to say about responses. Flask provides very good support for generating responses\n",
|
||
"using templates, and this is such an important topic that the next chapter is dedicated\n",
|
||
"to it.\n",
|
||
"Flask Extensions \n",
|
||
"| \n",
|
||
"23\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 3\n",
|
||
"Templates\n",
|
||
"The key to writing applications that are easy to maintain is to write clean and well-\n",
|
||
"structured code. The examples that you have seen so far are too simple to demon‐\n",
|
||
"strate this, but Flask view functions have two completely independent purposes\n",
|
||
"disguised as one, which creates a problem.\n",
|
||
"The obvious task of a view function is to generate a response to a request, as you have\n",
|
||
"seen in the examples shown in Chapter 2. For the simplest requests this is enough,\n",
|
||
"but in many cases a request also triggers a change in the state of the application, and\n",
|
||
"the view function is where this change is generated.\n",
|
||
"For example, consider a user who is registering a new account on a website. The user\n",
|
||
"types an email address and a password in a web form and clicks the Submit button.\n",
|
||
"On the server, a request with the data provided by the user arrives, and Flask dis‐\n",
|
||
"patches it to the view function that handles registration requests. This view function\n",
|
||
"needs to talk to the database to get the new user added, and then generate a response\n",
|
||
"to send back to the browser that includes a success or failure message. These two\n",
|
||
"types of tasks are formally called business logic and presentation logic, respectively.\n",
|
||
"Mixing business and presentation logic leads to code that is hard to understand and\n",
|
||
"maintain. Imagine having to build the HTML code for a large table by concatenating\n",
|
||
"data obtained from the database with the necessary HTML string literals. Moving the\n",
|
||
"presentation logic into templates helps improve the maintainability of the application.\n",
|
||
"A template is a file that contains the text of a response, with placeholder variables for\n",
|
||
"the dynamic parts that will be known only in the context of a request. The process\n",
|
||
"that replaces the variables with actual values and returns a final response string is\n",
|
||
"called rendering. For the task of rendering templates, Flask uses a powerful template\n",
|
||
"engine called Jinja2.\n",
|
||
"25\n",
|
||
"\n",
|
||
"The Jinja2 Template Engine\n",
|
||
"In its simplest form, a Jinja2 template is a file that contains the text of a response.\n",
|
||
"Example 3-1 shows a Jinja2 template that matches the response of the index() view\n",
|
||
"function of Example 2-1.\n",
|
||
"Example 3-1. templates/index.html: Jinja2 template\n",
|
||
"<h1>Hello World!</h1>\n",
|
||
"The response returned by the user() view function of Example 2-2 has a dynamic\n",
|
||
"component, which is represented by a variable. Example 3-2 shows the template that\n",
|
||
"implements this response.\n",
|
||
"Example 3-2. templates/user.html: Jinja2 template\n",
|
||
"<h1>Hello, {{ name }}!</h1>\n",
|
||
"Rendering Templates\n",
|
||
"By default Flask looks for templates in a templates subdirectory located inside the\n",
|
||
"main application directory. For the next version of hello.py, you need to create the\n",
|
||
"templates subdirectory and store the templates defined in the previous examples in it\n",
|
||
"as index.html and user.html, respectively.\n",
|
||
"The view functions in the application need to be modified to render these templates.\n",
|
||
"Example 3-3 shows these changes.\n",
|
||
"Example 3-3. hello.py: rendering a template\n",
|
||
"from flask import Flask, render_template\n",
|
||
"# ...\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" return render_template('index.html')\n",
|
||
"@app.route('/user/<name>')\n",
|
||
"def user(name):\n",
|
||
" return render_template('user.html', name=name)\n",
|
||
"26 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
"The function render_template() provided by Flask integrates the Jinja2 template\n",
|
||
"engine with the application. This function takes the filename of the template as its\n",
|
||
"first argument. Any additional arguments are key-value pairs that represent actual\n",
|
||
"values for variables referenced in the template. In this example, the second template is\n",
|
||
"receiving a name variable.\n",
|
||
"Keyword arguments like name=name in the previous example are fairly common, but\n",
|
||
"they may seem confusing and hard to understand if you are not used to them. The\n",
|
||
"“name” on the left side represents the argument name, which is used in the place‐\n",
|
||
"holder written in the template. The “name” on the right side is a variable in the cur‐\n",
|
||
"rent scope that provides the value for the argument of the same name. While this is a\n",
|
||
"common pattern, using the same variable name on both sides is not required.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 3a to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Variables\n",
|
||
"The {{ name }} construct used in the template shown in Example 3-2 references a\n",
|
||
"variable, a special placeholder that tells the template engine that the value that goes in\n",
|
||
"that place should be obtained from data provided at the time the template is ren‐\n",
|
||
"dered.\n",
|
||
"Jinja2 recognizes variables of any type, even complex types such as lists, dictionaries,\n",
|
||
"and objects. The following are some more examples of variables used in templates:\n",
|
||
"<p>A value from a dictionary: {{ mydict['key'] }}.</p>\n",
|
||
"<p>A value from a list: {{ mylist[3] }}.</p>\n",
|
||
"<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>\n",
|
||
"<p>A value from an object's method: {{ myobj.somemethod() }}.</p>\n",
|
||
"Variables can be modified with filters, which are added after the variable name with a\n",
|
||
"pipe character as separator. For example, the following template shows the name vari‐\n",
|
||
"able capitalized:\n",
|
||
"Hello, {{ name|capitalize }}\n",
|
||
"Table 3-1 lists some of the commonly used filters that come with Jinja2.\n",
|
||
"The Jinja2 Template Engine \n",
|
||
"| \n",
|
||
"27\n",
|
||
"\n",
|
||
"Table 3-1. Jinja2 variable filters\n",
|
||
"Filter name\n",
|
||
"Description\n",
|
||
"safe\n",
|
||
"Renders the value without applying escaping\n",
|
||
"capitalize\n",
|
||
"Converts the first character of the value to uppercase and the rest to lowercase\n",
|
||
"lower\n",
|
||
"Converts the value to lowercase characters\n",
|
||
"upper\n",
|
||
"Converts the value to uppercase characters\n",
|
||
"title\n",
|
||
"Capitalizes each word in the value\n",
|
||
"trim\n",
|
||
"Removes leading and trailing whitespace from the value\n",
|
||
"striptags\n",
|
||
"Removes any HTML tags from the value before rendering\n",
|
||
"The safe filter is interesting to highlight. By default Jinja2 escapes all variables for\n",
|
||
"security purposes. For example, if a variable is set to the value '<h1>Hello</h1>',\n",
|
||
"Jinja2 will render the string as '<h1>Hello</h1>', which will cause the\n",
|
||
"h1 element to be displayed and not interpreted by the browser. Many times it is neces‐\n",
|
||
"sary to display HTML code stored in variables, and for those cases the safe filter is\n",
|
||
"used.\n",
|
||
"Never use the safe filter on values that aren’t trusted, such as text\n",
|
||
"entered by users on web forms.\n",
|
||
"The complete list of filters can be obtained from the official Jinja2 documentation.\n",
|
||
"Control Structures\n",
|
||
"Jinja2 offers several control structures that can be used to alter the flow of the tem‐\n",
|
||
"plate. This section introduces some of the most useful ones with simple examples.\n",
|
||
"The following example shows how conditional statements can be entered in a\n",
|
||
"template:\n",
|
||
"{% if user %}\n",
|
||
" Hello, {{ user }}!\n",
|
||
"{% else %}\n",
|
||
" Hello, Stranger!\n",
|
||
"{% endif %}\n",
|
||
"Another common need in templates is to render a list of elements. This example\n",
|
||
"shows how this can be done with a for loop:\n",
|
||
"<ul>\n",
|
||
" {% for comment in comments %}\n",
|
||
"28 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
" <li>{{ comment }}</li>\n",
|
||
" {% endfor %}\n",
|
||
"</ul>\n",
|
||
"Jinja2 also supports macros, which are similar to functions in Python code. For\n",
|
||
"example:\n",
|
||
"{% macro render_comment(comment) %}\n",
|
||
" <li>{{ comment }}</li>\n",
|
||
"{% endmacro %}\n",
|
||
"<ul>\n",
|
||
" {% for comment in comments %}\n",
|
||
" {{ render_comment(comment) }}\n",
|
||
" {% endfor %}\n",
|
||
"</ul>\n",
|
||
"To make macros more reusable, they can be stored in standalone files that are then\n",
|
||
"imported from all the templates that need them:\n",
|
||
"{% import 'macros.html' as macros %}\n",
|
||
"<ul>\n",
|
||
" {% for comment in comments %}\n",
|
||
" {{ macros.render_comment(comment) }}\n",
|
||
" {% endfor %}\n",
|
||
"</ul>\n",
|
||
"Portions of template code that need to be repeated in several places can be stored in a \n",
|
||
"separate file and included from all the templates to avoid repetition:\n",
|
||
"{% include 'common.html' %}\n",
|
||
"Yet another powerful way to reuse is through template inheritance, which is similar to\n",
|
||
"class inheritance in Python code. First, a base template is created with the name\n",
|
||
"base.html:\n",
|
||
"<html>\n",
|
||
"<head>\n",
|
||
" {% block head %}\n",
|
||
" <title>{% block title %}{% endblock %} - My Application</title>\n",
|
||
" {% endblock %}\n",
|
||
"</head>\n",
|
||
"<body>\n",
|
||
" {% block body %}\n",
|
||
" {% endblock %}\n",
|
||
"</body>\n",
|
||
"</html>\n",
|
||
"Base templates define blocks that can be overridden by derived templates. The Jinja2\n",
|
||
"block and endblock directives define blocks of content that are added to the base\n",
|
||
"template. In this example, there are blocks called head, title, and body; note that\n",
|
||
"title is contained by head. The following example is a derived template of the base\n",
|
||
"template:\n",
|
||
"The Jinja2 Template Engine \n",
|
||
"| \n",
|
||
"29\n",
|
||
"\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% block title %}Index{% endblock %}\n",
|
||
"{% block head %}\n",
|
||
" {{ super() }}\n",
|
||
" <style>\n",
|
||
" </style>\n",
|
||
"{% endblock %}\n",
|
||
"{% block body %}\n",
|
||
"<h1>Hello, World!</h1>\n",
|
||
"{% endblock %}\n",
|
||
"The extends directive declares that this template derives from base.html. This direc‐\n",
|
||
"tive is followed by new definitions for the three blocks defined in the base template,\n",
|
||
"which are inserted in the proper places. When a block has some content in both the\n",
|
||
"base and derived templates, the content from the derived template is used. Within\n",
|
||
"this block, the derived template can call super() to reference the contents of the\n",
|
||
"block in the base template. In the preceding example, this is done in the head block.\n",
|
||
"Real-world usage of all the control structures presented in this section will be shown\n",
|
||
"later, so you will have the opportunity to see how they work.\n",
|
||
"Bootstrap Integration with Flask-Bootstrap\n",
|
||
"Bootstrap is an open-source web browser framework from Twitter that provides user\n",
|
||
"interface components that help create clean and attractive web pages that are compat‐\n",
|
||
"ible with all modern web browsers used on desktop and mobile platforms.\n",
|
||
"Bootstrap is a client-side framework, so the server is not directly involved with it. All\n",
|
||
"the server needs to do is provide HTML responses that reference Bootstrap’s Cascad‐\n",
|
||
"ing Style Sheets (CSS) and JavaScript files, and instantiate the desired user interface\n",
|
||
"elements through HTML, CSS, and JavaScript code. The ideal place to do all this is in\n",
|
||
"templates.\n",
|
||
"The naive approach to integrating Bootstrap with the application is to make all the\n",
|
||
"necessary changes to the HTML templates, following the recommendations given by\n",
|
||
"the Bootstrap documentation. But this is an area where the use of a Flask extension\n",
|
||
"makes an integration task much simpler, while helping keep these changes nicely\n",
|
||
"organized.\n",
|
||
"The extension is called Flask-Bootstrap, and it can be installed with pip:\n",
|
||
"(venv) $ pip install flask-bootstrap\n",
|
||
"Flask extensions are initialized at the same time the application instance is created.\n",
|
||
"Example 3-4 shows the initialization of Flask-Bootstrap.\n",
|
||
"30 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
"Example 3-4. hello.py: Flask-Bootstrap initialization\n",
|
||
"from flask_bootstrap import Bootstrap\n",
|
||
"# ...\n",
|
||
"bootstrap = Bootstrap(app)\n",
|
||
"The extension is usually imported from a flask_<name> package, where <name> is the\n",
|
||
"extension name. Most Flask extensions follow one of two consistent patterns for initi‐\n",
|
||
"alization. In Example 3-4, the extension is initialized by passing the application\n",
|
||
"instance as an argument in the constructor. You will learn about a more advanced\n",
|
||
"method to initialize extensions appropriate for larger applications in Chapter 7.\n",
|
||
"Once Flask-Bootstrap is initialized, a base template that includes all the Bootstrap\n",
|
||
"files and general structure is available to the application. The application then takes\n",
|
||
"advantage of Jinja2’s template inheritance to extend this base template. Example 3-5\n",
|
||
"shows a new version of user.html as a derived template.\n",
|
||
"Example 3-5. templates/user.html: template that uses Flask-Bootstrap\n",
|
||
"{% extends \"bootstrap/base.html\" %}\n",
|
||
"{% block title %}Flasky{% endblock %}\n",
|
||
"{% block navbar %}\n",
|
||
"<div class=\"navbar navbar-inverse\" role=\"navigation\">\n",
|
||
" <div class=\"container\">\n",
|
||
" <div class=\"navbar-header\">\n",
|
||
" <button type=\"button\" class=\"navbar-toggle\"\n",
|
||
" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n",
|
||
" <span class=\"sr-only\">Toggle navigation</span>\n",
|
||
" <span class=\"icon-bar\"></span>\n",
|
||
" <span class=\"icon-bar\"></span>\n",
|
||
" <span class=\"icon-bar\"></span>\n",
|
||
" </button>\n",
|
||
" <a class=\"navbar-brand\" href=\"/\">Flasky</a>\n",
|
||
" </div>\n",
|
||
" <div class=\"navbar-collapse collapse\">\n",
|
||
" <ul class=\"nav navbar-nav\">\n",
|
||
" <li><a href=\"/\">Home</a></li>\n",
|
||
" </ul>\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"{% block content %}\n",
|
||
"<div class=\"container\">\n",
|
||
" <div class=\"page-header\">\n",
|
||
" <h1>Hello, {{ name }}!</h1>\n",
|
||
" </div>\n",
|
||
"Bootstrap Integration with Flask-Bootstrap \n",
|
||
"| \n",
|
||
"31\n",
|
||
"\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"The Jinja2 extends directive implements the template inheritance by referencing\n",
|
||
"bootstrap/base.html from Flask-Bootstrap. The base template from Flask-Bootstrap\n",
|
||
"provides a skeleton web page that includes all the Bootstrap CSS and JavaScript files.\n",
|
||
"The user.html template defines three blocks called title, navbar, and content. These\n",
|
||
"are all blocks that the base template exports for derived templates to define. The\n",
|
||
"title block is straightforward; its contents will appear between <title> tags in the\n",
|
||
"header of the rendered HTML document. The navbar and content blocks are\n",
|
||
"reserved for the page navigation bar and main content.\n",
|
||
"In this template, the navbar block defines a simple navigation bar using Bootstrap\n",
|
||
"components. The content block has a container <div> with a page header inside. The\n",
|
||
"greeting line that was in the previous version of the template is now inside the page\n",
|
||
"header. Figure 3-1 shows how the application looks with these changes.\n",
|
||
"Figure 3-1. Bootstrap templates\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 3b to check out this version of the applica‐\n",
|
||
"tion. The Flask-Bootstrap package also needs to be installed in your\n",
|
||
"virtual environment. The Bootstrap official documentation is a\n",
|
||
"great learning resource full of copy/paste-ready examples.\n",
|
||
"Flask-Bootstrap’s base.html template defines several other blocks that can be used in\n",
|
||
"derived templates. Table 3-2 shows the complete list of available blocks.\n",
|
||
"32 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
"Table 3-2. Flask-Bootstrap’s base template blocks\n",
|
||
"Block name\n",
|
||
"Description\n",
|
||
"doc\n",
|
||
"The entire HTML document\n",
|
||
"html_attribs\n",
|
||
"Attributes inside the <html> tag\n",
|
||
"html\n",
|
||
"The contents of the <html> tag\n",
|
||
"head\n",
|
||
"The contents of the <head> tag\n",
|
||
"title\n",
|
||
"The contents of the <title> tag\n",
|
||
"metas\n",
|
||
"The list of <meta> tags\n",
|
||
"styles\n",
|
||
"CSS definitions\n",
|
||
"body_attribs Attributes inside the <body> tag\n",
|
||
"body\n",
|
||
"The contents of the <body> tag\n",
|
||
"navbar\n",
|
||
"User-defined navigation bar\n",
|
||
"content\n",
|
||
"User-defined page content\n",
|
||
"scripts\n",
|
||
"JavaScript declarations at the bottom of the document\n",
|
||
"Many of the blocks in Table 3-2 are used by Flask-Bootstrap itself, so overriding them\n",
|
||
"directly would cause problems. For example, the styles and scripts blocks are\n",
|
||
"where the Bootstrap CSS and JavaScript files are declared. If the application needs to\n",
|
||
"add its own content to a block that already has some content, then Jinja2’s super()\n",
|
||
"function must be used. For example, this is how the scripts block would need to be\n",
|
||
"written in the derived template to add a new JavaScript file to the document:\n",
|
||
"{% block scripts %}\n",
|
||
"{{ super() }}\n",
|
||
"<script type=\"text/javascript\" src=\"my-script.js\"></script>\n",
|
||
"{% endblock %}\n",
|
||
"Custom Error Pages\n",
|
||
"When you enter an invalid route in your browser’s address bar, you get a code 404\n",
|
||
"error page. Compared to the Bootstrap-powered pages, the default error page is now\n",
|
||
"too plain and unattractive, and it has no consistency with the actual pages generated\n",
|
||
"by the application.\n",
|
||
"Flask allows an application to define custom error pages that can be based on tem‐\n",
|
||
"plates, like regular routes. The two most common error codes are 404, triggered when\n",
|
||
"the client requests a page or route that is not known, and 500, triggered when there is\n",
|
||
"an unhandled exception in the application. Example 3-6 shows how to provide cus‐\n",
|
||
"tom handlers for these two errors using the app.errorhandler decorator.\n",
|
||
"Custom Error Pages \n",
|
||
"| \n",
|
||
"33\n",
|
||
"\n",
|
||
"Example 3-6. hello.py: custom error pages\n",
|
||
"@app.errorhandler(404)\n",
|
||
"def page_not_found(e):\n",
|
||
" return render_template('404.html'), 404\n",
|
||
"@app.errorhandler(500)\n",
|
||
"def internal_server_error(e):\n",
|
||
" return render_template('500.html'), 500\n",
|
||
"Error handlers return a response, like view functions, but they also need to return the\n",
|
||
"numeric status code that corresponds to the error, which Flask conveniently accepts\n",
|
||
"as a second return value.\n",
|
||
"The templates referenced in the error handlers need to be written. These templates\n",
|
||
"should follow the same layout as the regular pages, so in this case they will have a\n",
|
||
"navigation bar and a page header that shows the error message.\n",
|
||
"The straightforward way to write these templates is to copy templates/user.html to\n",
|
||
"templates/404.html and templates/500.html and then change the page header elements\n",
|
||
"in these two new files to the appropriate error messages, but this will generate a lot of\n",
|
||
"duplication.\n",
|
||
"Jinja2’s template inheritance can help with this. In the same way Flask-Bootstrap pro‐\n",
|
||
"vides a base template with the basic layout of the page, the application can define its\n",
|
||
"own base template with a uniform page layout that includes the navigation bar and\n",
|
||
"leaves the page content to be defined in derived templates. Example 3-7 shows tem‐\n",
|
||
"plates/base.html, a new template that inherits from bootstrap/base.html and defines\n",
|
||
"the navigation bar but is itself a second-level base template to other templates such as\n",
|
||
"templates/user.html, templates/404.html, and templates/500.html.\n",
|
||
"Example 3-7. templates/base.html: base application template with navigation bar\n",
|
||
"{% extends \"bootstrap/base.html\" %}\n",
|
||
"{% block title %}Flasky{% endblock %}\n",
|
||
"{% block navbar %}\n",
|
||
"<div class=\"navbar navbar-inverse\" role=\"navigation\">\n",
|
||
" <div class=\"container\">\n",
|
||
" <div class=\"navbar-header\">\n",
|
||
" <button type=\"button\" class=\"navbar-toggle\"\n",
|
||
" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n",
|
||
" <span class=\"sr-only\">Toggle navigation</span>\n",
|
||
" <span class=\"icon-bar\"></span>\n",
|
||
" <span class=\"icon-bar\"></span>\n",
|
||
" <span class=\"icon-bar\"></span>\n",
|
||
" </button>\n",
|
||
" <a class=\"navbar-brand\" href=\"/\">Flasky</a>\n",
|
||
"34 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
" </div>\n",
|
||
" <div class=\"navbar-collapse collapse\">\n",
|
||
" <ul class=\"nav navbar-nav\">\n",
|
||
" <li><a href=\"/\">Home</a></li>\n",
|
||
" </ul>\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"{% block content %}\n",
|
||
"<div class=\"container\">\n",
|
||
" {% block page_content %}{% endblock %}\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"The content block of this template is just a container <div> element that wraps a new\n",
|
||
"empty block called page_content, which derived templates can define.\n",
|
||
"The templates of the application will now inherit from this template instead of\n",
|
||
"directly from Flask-Bootstrap. Example 3-8 shows how simple it is to construct a cus‐\n",
|
||
"tom code 404 error page that inherits from templates/base.html. The page for the 500\n",
|
||
"error is similar, and you can find it in the GitHub repository for the application.\n",
|
||
"Example 3-8. templates/404.html: custom code 404 error page using template\n",
|
||
"inheritance\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% block title %}Flasky - Page Not Found{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>Not Found</h1>\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"Figure 3-2 shows how the error page looks in the browser.\n",
|
||
"Custom Error Pages \n",
|
||
"| \n",
|
||
"35\n",
|
||
"\n",
|
||
"Figure 3-2. Custom code 404 error page\n",
|
||
"The templates/user.html template can now be simplified by making it inherit from the\n",
|
||
"base template, as shown in Example 3-9.\n",
|
||
"Example 3-9. templates/user.html: simplified page template using template inheritance\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% block title %}Flasky{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>Hello, {{ name }}!</h1>\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 3c to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Links\n",
|
||
"Any application that has more than one route will invariably need to include links\n",
|
||
"that connect the different pages, such as in a navigation bar.\n",
|
||
"Writing the URLs as links directly in the template is trivial for simple routes, but for\n",
|
||
"dynamic routes with variable portions it can get more complicated to build the URLs\n",
|
||
"36 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
"right in the template. Also, URLs written explicitly create an unwanted dependency\n",
|
||
"on the routes defined in the code. If the routes are reorganized, links in templates\n",
|
||
"may break.\n",
|
||
"To avoid these problems, Flask provides the url_for() helper function, which gener‐\n",
|
||
"ates URLs from the information stored in the application’s URL map.\n",
|
||
"In its simplest usage, this function takes the view function name (or endpoint name\n",
|
||
"for routes defined with app.add_url_route()) as its single argument and returns its\n",
|
||
"URL. For example, in the current version of hello.py the call url_for('index')\n",
|
||
"would return /, the root URL of the application. Calling url_for('index',\n",
|
||
"_external=True) would instead return an absolute URL, which in this example is\n",
|
||
"http://localhost:5000/.\n",
|
||
"Relative URLs are sufficient when generating links that connect the\n",
|
||
"different routes of the application. Absolute URLs are necessary\n",
|
||
"only for links that will be used outside of the web browser, such as\n",
|
||
"when sending links by email.\n",
|
||
"Dynamic URLs can be generated with url_for() by passing the dynamic parts as\n",
|
||
"keyword \n",
|
||
"arguments. \n",
|
||
"For \n",
|
||
"example, \n",
|
||
"url_for('user', \n",
|
||
"name='john',\n",
|
||
"_external=True) would return http://localhost:5000/user/john.\n",
|
||
"Keyword arguments sent to url_for() are not limited to arguments used by dynamic\n",
|
||
"routes. The function will add any arguments that are not dynamic to the query string.\n",
|
||
"For example, url_for('user', name='john', page=2, version=1) would\n",
|
||
"return /user/john?page=2&version=1.\n",
|
||
"Static Files\n",
|
||
"Web applications are not made of Python code and templates alone. Most applica‐\n",
|
||
"tions also use static files such as images, JavaScript source files, and CSS files that are\n",
|
||
"all referenced from the HTML code in templates.\n",
|
||
"You may recall that when the hello.py application’s URL map was inspected in Chap‐\n",
|
||
"ter 2, a static entry appeared in it. Flask automatically supports static files by adding\n",
|
||
"a special route to the application defined as /static/<filename>. For example, a call\n",
|
||
"to url_for('static', filename='css/styles.css', _external=True) would\n",
|
||
"return http://localhost:5000/static/css/styles.css.\n",
|
||
"In its default configuration, Flask looks for static files in a subdirectory called static\n",
|
||
"located in the application’s root folder. Files can be organized in subdirectories inside\n",
|
||
"this folder if desired. When the server receives a URL that maps to a static route, it\n",
|
||
"Static Files \n",
|
||
"| \n",
|
||
"37\n",
|
||
"\n",
|
||
"generates a response that includes the contents of the corresponding file in the file\n",
|
||
"system.\n",
|
||
"Example 3-10 shows how the application can include a favicon.ico icon in the base\n",
|
||
"template for browsers to show in the address bar.\n",
|
||
"Example 3-10. templates/base.html: favicon definition\n",
|
||
"{% block head %}\n",
|
||
"{{ super() }}\n",
|
||
"<link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\"\n",
|
||
" type=\"image/x-icon\">\n",
|
||
"<link rel=\"icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\"\n",
|
||
" type=\"image/x-icon\">\n",
|
||
"{% endblock %}\n",
|
||
"The icon declaration is inserted at the end of the head block. Note how super() is\n",
|
||
"used to preserve the original contents of the block defined in the base templates.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 3d to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Localization of Dates and Times with Flask-Moment\n",
|
||
"Handling of dates and times in a web application is not a trivial problem when users\n",
|
||
"work in different parts of the world.\n",
|
||
"The server needs uniform time units that are independent of the location of each\n",
|
||
"user, so typically Coordinated Universal Time (UTC) is used. For users, however, see‐\n",
|
||
"ing times expressed in UTC can be confusing, as users always expect to see dates and\n",
|
||
"times presented in their local time and formatted according to the customs of their\n",
|
||
"region.\n",
|
||
"An elegant solution that allows the server to work exclusively in UTC is to send these\n",
|
||
"time units to the web browser, where they are converted to local time and rendered\n",
|
||
"using JavaScript. Web browsers can do a much better job at this task because they\n",
|
||
"have access to time zone and locale settings on the user’s computer.\n",
|
||
"There is an excellent open source library written in JavaScript that renders dates and\n",
|
||
"times in the browser called Moment.js. Flask-Moment is an extension for Flask appli‐\n",
|
||
"cations that makes the integration of Moment.js into Jinja2 templates very easy. Flask-\n",
|
||
"Moment is installed with pip:\n",
|
||
"(venv) $ pip install flask-moment\n",
|
||
"38 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
"The extension is initialized in a similar way to Flask-Bootstrap. The required code is\n",
|
||
"shown in Example 3-11.\n",
|
||
"Example 3-11. hello.py: initializing Flask-Moment\n",
|
||
"from flask_moment import Moment\n",
|
||
"moment = Moment(app)\n",
|
||
"Flask-Moment depends on jQuery.js in addition to Moment.js. These two libraries\n",
|
||
"need to be included somewhere in the HTML document—either directly, in which\n",
|
||
"case you can choose what versions to use, or through the helper functions provided\n",
|
||
"by the extension, which reference tested versions of these libraries from a content\n",
|
||
"delivery network (CDN). Because Bootstrap already includes jQuery.js, only\n",
|
||
"Moment.js needs to be added in this case. Example 3-12 shows how this library is\n",
|
||
"loaded in the scripts block of the template, while also preserving the original con‐\n",
|
||
"tents of the block provided by the base template. Note that since this is a predefined\n",
|
||
"block in the Flask-Bootstrap base template, the location in templates/base.html where\n",
|
||
"this block is inserted does not matter.\n",
|
||
"Example 3-12. templates/base.html: importing the Moment.js library\n",
|
||
"{% block scripts %}\n",
|
||
"{{ super() }}\n",
|
||
"{{ moment.include_moment() }}\n",
|
||
"{% endblock %}\n",
|
||
"To work with timestamps, Flask-Moment makes a moment object available to tem‐\n",
|
||
"plates. Example 3-13 demonstrates passing a variable called current_time to the tem‐\n",
|
||
"plate for rendering.\n",
|
||
"Example 3-13. hello.py: adding a datetime variable\n",
|
||
"from datetime import datetime\n",
|
||
"@app.route('/')\n",
|
||
"def index():\n",
|
||
" return render_template('index.html',\n",
|
||
" current_time=datetime.utcnow())\n",
|
||
"Example 3-14 shows how this current_time template variable is rendered.\n",
|
||
"Example 3-14. templates/index.html: timestamp rendering with Flask-Moment\n",
|
||
"<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>\n",
|
||
"<p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>\n",
|
||
"Localization of Dates and Times with Flask-Moment \n",
|
||
"| \n",
|
||
"39\n",
|
||
"\n",
|
||
"The format('LLL') function renders the date and time according to the time zone\n",
|
||
"and locale settings in the client computer. The argument determines the rendering\n",
|
||
"style, from 'L' to 'LLLL' for four different levels of verbosity. The format() function\n",
|
||
"can also accept a long list of custom format specifiers.\n",
|
||
"The fromNow() render style shown in the second line renders a relative timestamp\n",
|
||
"and automatically refreshes it as time passes. Initially this timestamp will be shown as\n",
|
||
"“a few seconds ago,” but the refresh=True option will keep it updated as time passes,\n",
|
||
"so if you leave the page open for a few minutes you will see the text changing to “a\n",
|
||
"minute ago,” then “2 minutes ago,” and so on.\n",
|
||
"Figure 3-3 shows how the http://localhost:5000/ route looks after the two timestamps\n",
|
||
"are added to the index.html template.\n",
|
||
"Figure 3-3. Page with two Flask-Moment timestamps\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 3e to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Flask-Moment implements the format(), fromNow(), fromTime(), calendar(),\n",
|
||
"valueOf(), and unix() methods from Moment.js. Consult the Moment.js documen‐\n",
|
||
"tation to learn about all the formatting options offered by this library.\n",
|
||
"40 \n",
|
||
"| \n",
|
||
"Chapter 3: Templates\n",
|
||
"\n",
|
||
"Flask-Moment assumes that timestamps handled by the server-side\n",
|
||
"application are “naive” datetime objects expressed in UTC. See the\n",
|
||
"documentation for the datetime package in the standard library\n",
|
||
"for information on naive and aware date and time objects.\n",
|
||
"The timestamps rendered by Flask-Moment can be localized to many languages. A\n",
|
||
"language can be selected in the template by passing the two-letter language code to\n",
|
||
"function locale(), right after the Moment.js library is included. For example, here is\n",
|
||
"how to configure Moment.js to use Spanish:\n",
|
||
"{% block scripts %}\n",
|
||
"{{ super() }}\n",
|
||
"{{ moment.include_moment() }}\n",
|
||
"{{ moment.locale('es') }}\n",
|
||
"{% endblock %}\n",
|
||
"With all the techniques discussed in this chapter, you should be able to build modern\n",
|
||
"and user-friendly web pages for your application. The next chapter touches on an\n",
|
||
"aspect of templates not yet discussed: how to interact with the user through web\n",
|
||
"forms.\n",
|
||
"Localization of Dates and Times with Flask-Moment \n",
|
||
"| \n",
|
||
"41\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 4\n",
|
||
"Web Forms\n",
|
||
"The templates that you worked with in Chapter 3 are unidirectional, in the sense that\n",
|
||
"they allow information to flow from the server to the user. For most applications,\n",
|
||
"however, there is also a need to have information that flows in the other direction,\n",
|
||
"with the user providing data that the server accepts and processes.\n",
|
||
"With HTML, it is possible to create web forms, in which users can enter information.\n",
|
||
"The form data is then submitted by the web browser to the server, typically in the\n",
|
||
"form of a POST request. The Flask request object, introduced in Chapter 2, exposes all\n",
|
||
"the information sent by the client in a request and, in particular for POST requests\n",
|
||
"containing form data, provides access to the user information through request.form.\n",
|
||
"Although the support provided in Flask’s request object is sufficient for the handling\n",
|
||
"of web forms, there are a number of tasks that can become tedious and repetitive.\n",
|
||
"Two good examples are the generation of HTML code for the forms and the valida‐\n",
|
||
"tion of the submitted form data.\n",
|
||
"The Flask-WTF extension makes working with web forms a much more pleasant\n",
|
||
"experience. This extension is a Flask integration wrapper around the framework-\n",
|
||
"agnostic WTForms package.\n",
|
||
"Flask-WTF and its dependencies can be installed with pip:\n",
|
||
"(venv) $ pip install flask-wtf\n",
|
||
"43\n",
|
||
"\n",
|
||
"Configuration\n",
|
||
"Unlike most other extensions, Flask-WTF does not need to be initialized at the appli‐\n",
|
||
"cation level, but it expects the application to have a secret key configured. A secret key\n",
|
||
"is a string with any random and unique content that is used as an encryption or sign‐\n",
|
||
"ing key to improve the security of the application in several ways. Flask uses this key\n",
|
||
"to protect the contents of the user session against tampering. You should pick a dif‐\n",
|
||
"ferent secret key in each application that you build and make sure that this string is\n",
|
||
"not known by anyone. Example 4-1 shows how to configure a secret key in a Flask\n",
|
||
"application.\n",
|
||
"Example 4-1. hello.py: Flask-WTF configuration\n",
|
||
"app = Flask(__name__)\n",
|
||
"app.config['SECRET_KEY'] = 'hard to guess string'\n",
|
||
"The app.config dictionary is a general-purpose place to store configuration variables\n",
|
||
"used by Flask, extensions, or the application itself. Configuration values can be added\n",
|
||
"to the app.config object using standard dictionary syntax. The configuration object\n",
|
||
"also has methods to import configuration values from files or the environment. A\n",
|
||
"more practical way to manage configuration values for a larger application will be\n",
|
||
"discussed in Chapter 7.\n",
|
||
"Flask-WTF requires a secret key to be configured in the application because this key\n",
|
||
"is part of the mechanism the extension uses to protect all forms against cross-site\n",
|
||
"request forgery (CSRF) attacks. A CSRF attack occurs when a malicious website sends\n",
|
||
"requests to the application server on which the user is currently logged in. Flask-WTF\n",
|
||
"generates security tokens for all forms and stores them in the user session, which is\n",
|
||
"protected with a cryptographic signature generated from the secret key.\n",
|
||
"For added security, the secret key should be stored in an environ‐\n",
|
||
"ment variable instead of being embedded in the source code. This\n",
|
||
"technique is described in Chapter 7.\n",
|
||
"Form Classes\n",
|
||
"When using Flask-WTF, each web form is represented in the server by a class that\n",
|
||
"inherits from the class FlaskForm. The class defines the list of fields in the form, each\n",
|
||
"represented by an object. Each field object can have one or more validators attached.\n",
|
||
"A validator is a function that checks whether the data submitted by the user is valid.\n",
|
||
"44 \n",
|
||
"| \n",
|
||
"Chapter 4: Web Forms\n",
|
||
"\n",
|
||
"Example 4-2 shows a simple web form that has a text field and a submit button.\n",
|
||
"Example 4-2. hello.py: form class definition\n",
|
||
"from flask_wtf import FlaskForm\n",
|
||
"from wtforms import StringField, SubmitField\n",
|
||
"from wtforms.validators import DataRequired\n",
|
||
"class NameForm(FlaskForm):\n",
|
||
" name = StringField('What is your name?', validators=[DataRequired()])\n",
|
||
" submit = SubmitField('Submit')\n",
|
||
"The fields in the form are defined as class variables, and each class variable is assigned\n",
|
||
"an object associated with the field type. In this example, the NameForm form has a text\n",
|
||
"field called name and a submit button called submit. The StringField class repre‐\n",
|
||
"sents an HTML <input> element with a type=\"text\" attribute. The SubmitField\n",
|
||
"class represents an HTML <input> element with a type=\"submit\" attribute. The first\n",
|
||
"argument to the field constructors is the label that will be used when rendering the\n",
|
||
"form to HTML.\n",
|
||
"The optional validators argument included in the StringField constructor defines\n",
|
||
"a list of checkers that will be applied to the data submitted by the user before it is\n",
|
||
"accepted. The DataRequired() validator ensures that the field is not submitted\n",
|
||
"empty.\n",
|
||
"The FlaskForm base class is defined by the Flask-WTF extension,\n",
|
||
"so it is imported from flask_wtf. The fields and validators, how‐\n",
|
||
"ever, are imported directly from the WTForms package.\n",
|
||
"The list of standard HTML fields supported by WTForms is shown in Table 4-1.\n",
|
||
"Table 4-1. WTForms standard HTML fields\n",
|
||
"Field type\n",
|
||
"Description\n",
|
||
"BooleanField\n",
|
||
"Checkbox with True and False values\n",
|
||
"DateField\n",
|
||
"Text field that accepts a datetime.date value in a given format\n",
|
||
"DateTimeField\n",
|
||
"Text field that accepts a datetime.datetime value in a given format\n",
|
||
"DecimalField\n",
|
||
"Text field that accepts a decimal.Decimal value\n",
|
||
"FileField\n",
|
||
"File upload field\n",
|
||
"HiddenField\n",
|
||
"Hidden text field\n",
|
||
"MultipleFileField\n",
|
||
"Multiple file upload field\n",
|
||
"FieldList\n",
|
||
"List of fields of a given type\n",
|
||
"Form Classes \n",
|
||
"| \n",
|
||
"45\n",
|
||
"\n",
|
||
"Field type\n",
|
||
"Description\n",
|
||
"FloatField\n",
|
||
"Text field that accepts a floating-point value\n",
|
||
"FormField\n",
|
||
"Form embedded as a field in a container form\n",
|
||
"IntegerField\n",
|
||
"Text field that accepts an integer value\n",
|
||
"PasswordField\n",
|
||
"Password text field\n",
|
||
"RadioField\n",
|
||
"List of radio buttons\n",
|
||
"SelectField\n",
|
||
"Drop-down list of choices\n",
|
||
"SelectMultipleField\n",
|
||
"Drop-down list of choices with multiple selection\n",
|
||
"SubmitField\n",
|
||
"Form submission button\n",
|
||
"StringField\n",
|
||
"Text field\n",
|
||
"TextAreaField\n",
|
||
"Multiple-line text field\n",
|
||
"The list of WTForms built-in validators is shown in Table 4-2.\n",
|
||
"Table 4-2. WTForms validators\n",
|
||
"Validator\n",
|
||
"Description\n",
|
||
"DataRequired\n",
|
||
"Validates that the field contains data after type conversion\n",
|
||
"Email\n",
|
||
"Validates an email address\n",
|
||
"EqualTo\n",
|
||
"Compares the values of two fields; useful when requesting a password to be entered twice for\n",
|
||
"confirmation\n",
|
||
"InputRequired Validates that the field contains data before type conversion\n",
|
||
"IPAddress\n",
|
||
"Validates an IPv4 network address\n",
|
||
"Length\n",
|
||
"Validates the length of the string entered\n",
|
||
"MacAddress\n",
|
||
"Validates a MAC address\n",
|
||
"NumberRange\n",
|
||
"Validates that the value entered is within a numeric range\n",
|
||
"Optional\n",
|
||
"Allows empty input in the field, skipping additional validators\n",
|
||
"Regexp\n",
|
||
"Validates the input against a regular expression\n",
|
||
"URL\n",
|
||
"Validates a URL\n",
|
||
"UUID\n",
|
||
"Validates a UUID\n",
|
||
"AnyOf\n",
|
||
"Validates that the input is one of a list of possible values\n",
|
||
"NoneOf\n",
|
||
"Validates that the input is none of a list of possible values\n",
|
||
"46 \n",
|
||
"| \n",
|
||
"Chapter 4: Web Forms\n",
|
||
"\n",
|
||
"HTML Rendering of Forms\n",
|
||
"Form fields are callables that, when invoked from a template, render themselves to\n",
|
||
"HTML. Assuming that the view function passes a NameForm instance to the template\n",
|
||
"as an argument named form, the template can generate a simple HTML form as\n",
|
||
"follows:\n",
|
||
"<form method=\"POST\">\n",
|
||
" {{ form.hidden_tag() }}\n",
|
||
" {{ form.name.label }} {{ form.name() }}\n",
|
||
" {{ form.submit() }}\n",
|
||
"</form>\n",
|
||
"Note that in addition to the name and submit fields, the form has a\n",
|
||
"form.hidden_tag() element. This element defines an extra form field that is hidden,\n",
|
||
"used by Flask-WTF to implement CSRF protection.\n",
|
||
"Of course, the result of rendering a web form in this way is extremely bare. Any key‐\n",
|
||
"word arguments added to the calls that render the fields are converted into HTML\n",
|
||
"attributes for the field—so, for example, you can give the field id or class attributes\n",
|
||
"and then define CSS styles for them:\n",
|
||
"<form method=\"POST\">\n",
|
||
" {{ form.hidden_tag() }}\n",
|
||
" {{ form.name.label }} {{ form.name(id='my-text-field') }}\n",
|
||
" {{ form.submit() }}\n",
|
||
"</form>\n",
|
||
"But even with HTML attributes, the effort required to render a form in this way and\n",
|
||
"make it look good is significant, so it is best to leverage Bootstrap’s own set of form\n",
|
||
"styles whenever possible. The Flask-Bootstrap extension provides a high-level helper\n",
|
||
"function that renders an entire Flask-WTF form using Bootstrap’s predefined form\n",
|
||
"styles, all with a single call. Using Flask-Bootstrap, the previous form can be rendered\n",
|
||
"as follows:\n",
|
||
"{% import \"bootstrap/wtf.html\" as wtf %}\n",
|
||
"{{ wtf.quick_form(form) }}\n",
|
||
"The import directive works in the same way as regular Python scripts do and allows\n",
|
||
"template elements to be imported and used in many templates. The imported boot‐\n",
|
||
"strap/wtf.html file defines helper functions that render Flask-WTF forms using Boot‐\n",
|
||
"strap. The wtf.quick_form() function takes a Flask-WTF form object and renders it\n",
|
||
"using default Bootstrap styles. The complete template for hello.py is shown in\n",
|
||
"Example 4-3.\n",
|
||
"HTML Rendering of Forms \n",
|
||
"| \n",
|
||
"47\n",
|
||
"\n",
|
||
"Example 4-3. templates/index.html: using Flask-WTF and Flask-Bootstrap to render a\n",
|
||
"form\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% import \"bootstrap/wtf.html\" as wtf %}\n",
|
||
"{% block title %}Flasky{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>\n",
|
||
"</div>\n",
|
||
"{{ wtf.quick_form(form) }}\n",
|
||
"{% endblock %}\n",
|
||
"The content area of the template now has two sections. The first section is a page\n",
|
||
"header that shows a greeting. Here a template conditional is used. Conditionals in\n",
|
||
"Jinja2 have the format {% if condition %}...{% else %}...{% endif %}. If the\n",
|
||
"condition evaluates to True, then what appears between the if and else directives is\n",
|
||
"added to the rendered template. If the condition evaluates to False, then what’s\n",
|
||
"between the else and endif is rendered instead. The purpose of this is to render\n",
|
||
"Hello, {{ name }}! when the name template variable is defined, or the string Hello,\n",
|
||
"Stranger! when it is not. The second section of the content renders the NameForm\n",
|
||
"form using the wtf.quick_form() function.\n",
|
||
"Form Handling in View Functions\n",
|
||
"In the new version of hello.py, the index() view function will have two tasks. First it\n",
|
||
"will render the form, and then it will receive the form data entered by the user.\n",
|
||
"Example 4-4 shows the updated index() view function.\n",
|
||
"Example 4-4. hello.py: handle a web form with GET and POST request methods\n",
|
||
"@app.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" name = None\n",
|
||
" form = NameForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" name = form.name.data\n",
|
||
" form.name.data = ''\n",
|
||
" return render_template('index.html', form=form, name=name)\n",
|
||
"The methods argument added to the app.route decorator tells Flask to register the\n",
|
||
"view function as a handler for GET and POST requests in the URL map. When methods\n",
|
||
"is not given, the view function is registered to handle GET requests only.\n",
|
||
"48 \n",
|
||
"| \n",
|
||
"Chapter 4: Web Forms\n",
|
||
"\n",
|
||
"Adding POST to the method list is necessary because form submissions are much\n",
|
||
"more conveniently handled as POST requests. It is possible to submit a form as a GET\n",
|
||
"request, but as GET requests have no body, the data is appended to the URL as a query\n",
|
||
"string and becomes visible in the browser’s address bar. For this and several other rea‐\n",
|
||
"sons, form submissions are almost universally done as POST requests.\n",
|
||
"The local name variable is used to hold the name received from the form when avail‐\n",
|
||
"able; when the name is not known, the variable is initialized to None. The view func‐\n",
|
||
"tion creates an instance of the NameForm class shown previously to represent the form. \n",
|
||
"The validate_on_submit() method of the form returns True when the form was\n",
|
||
"submitted and the data was accepted by all the field validators. In all other cases,\n",
|
||
"validate_on_submit() returns False. The return value of this method effectively\n",
|
||
"serves to determine whether the form needs to be rendered or processed.\n",
|
||
"When a user navigates to the application for the first time, the server will receive a\n",
|
||
"GET request with no form data, so validate_on_submit() will return False. The\n",
|
||
"body of the if statement will be skipped and the request will be handled by rendering\n",
|
||
"the template, which gets the form object and the name variable set to None as argu‐\n",
|
||
"ments. Users will now see the form displayed in the browser.\n",
|
||
"When the form is submitted by the user, the server receives a POST request with the\n",
|
||
"data. The call to validate_on_submit() invokes the DataRequired() validator\n",
|
||
"attached to the name field. If the name is not empty, then the validator accepts it and\n",
|
||
"validate_on_submit() returns True. Now the name entered by the user is accessible\n",
|
||
"as the data attribute of the field. Inside the body of the if statement, this name is\n",
|
||
"assigned to the local name variable and the form field is cleared by setting that data\n",
|
||
"attribute to an empty string, so that the field is blanked when the form is rendered to\n",
|
||
"the page again. The render_template() call in the last line renders the template, but\n",
|
||
"this time the name argument contains the name from the form, so the greeting will be\n",
|
||
"personalized.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 4a to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Figure 4-1 shows how the form looks in the browser window when a user initially\n",
|
||
"enters the site. When the user submits a name, the application responds with a per‐\n",
|
||
"sonalized greeting. The form still appears below it, so a user can submit it multiple\n",
|
||
"times with different names if desired. Figure 4-2 shows the application in this state.\n",
|
||
"Form Handling in View Functions \n",
|
||
"| \n",
|
||
"49\n",
|
||
"\n",
|
||
"Figure 4-1. Flask-WTF web form\n",
|
||
"Figure 4-2. Web form after submission\n",
|
||
"50 \n",
|
||
"| \n",
|
||
"Chapter 4: Web Forms\n",
|
||
"\n",
|
||
"If the user submits the form with an empty name, the DataRequired() validator\n",
|
||
"catches the error, as seen in Figure 4-3. Note how much functionality is being pro‐\n",
|
||
"vided automatically. This is a great example of the power that well-designed exten‐\n",
|
||
"sions like Flask-WTF and Flask-Bootstrap can give to your application.\n",
|
||
"Figure 4-3. Web form after failed validator\n",
|
||
"Redirects and User Sessions\n",
|
||
"The last version of hello.py has a usability problem. If you enter your name and sub‐\n",
|
||
"mit it, and then click the refresh button in your browser, you will likely get an\n",
|
||
"obscure warning that asks for confirmation before submitting the form again. This\n",
|
||
"happens because browsers repeat the last request they sent when they are asked to\n",
|
||
"refresh a page. When the last request sent is a POST request with form data, a refresh\n",
|
||
"would cause a duplicate form submission, which in almost all cases is not the desired\n",
|
||
"action. For that reason, the browser asks for confirmation from the user.\n",
|
||
"Many users do not understand this warning from the browser. Consequently, it is\n",
|
||
"considered good practice for web applications to never leave a POST request as the last\n",
|
||
"request sent by the browser.\n",
|
||
"This is achieved by responding to POST requests with a redirect instead of a normal\n",
|
||
"response. A redirect is a special type of response that contains a URL instead of a\n",
|
||
"string with HTML code. When the browser receives a redirect response, it issues a\n",
|
||
"Redirects and User Sessions \n",
|
||
"| \n",
|
||
"51\n",
|
||
"\n",
|
||
"GET request for the redirect URL, and that is the page that it displays. The page may\n",
|
||
"take a few more milliseconds to load because of the second request that has to be sent\n",
|
||
"to the server, but other than that, the user will not see any difference. Now the last\n",
|
||
"request is a GET, so the refresh command works as expected. This trick is known as\n",
|
||
"the Post/Redirect/Get pattern.\n",
|
||
"But this approach brings a second problem. When the application handles the POST\n",
|
||
"request, it has access to the name entered by the user in form.name.data, but as soon\n",
|
||
"as that request ends the form data is lost. Because the POST request is handled with a\n",
|
||
"redirect, the application needs to store the name so that the redirected request can\n",
|
||
"have it and use it to build the actual response.\n",
|
||
"Applications can “remember” things from one request to the next by storing them in\n",
|
||
"the user session, a private storage that is available to each connected client. The user\n",
|
||
"session was introduced in Chapter 2 as one of the variables associated with the\n",
|
||
"request context. It’s called session and is accessed like a standard Python dictionary.\n",
|
||
"By default, user sessions are stored in client-side cookies that are\n",
|
||
"cryptographically signed using the configured secret key. Any tam‐\n",
|
||
"pering with the cookie content would render the signature invalid,\n",
|
||
"thus invalidating the session.\n",
|
||
"Example 4-5 shows a new version of the index() view function that implements redi‐\n",
|
||
"rects and user sessions.\n",
|
||
"Example 4-5. hello.py: redirects and user sessions\n",
|
||
"from flask import Flask, render_template, session, redirect, url_for\n",
|
||
"@app.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" form = NameForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" session['name'] = form.name.data\n",
|
||
" return redirect(url_for('index'))\n",
|
||
" return render_template('index.html', form=form, name=session.get('name'))\n",
|
||
"In the previous version of the application, a local name variable was used to store the\n",
|
||
"name entered by the user in the form. That variable is now placed in the user session\n",
|
||
"as session['name'] so that it is remembered beyond the request.\n",
|
||
"52 \n",
|
||
"| \n",
|
||
"Chapter 4: Web Forms\n",
|
||
"\n",
|
||
"Requests that come with valid form data will now end with a call to redirect(), a\n",
|
||
"Flask helper function that generates the HTTP redirect response. The redirect()\n",
|
||
"function takes the URL to redirect to as an argument. The redirect URL used in this\n",
|
||
"case is the root URL, so the response could have been written more concisely as\n",
|
||
"redirect('/'), but instead Flask’s URL generator function url_for(), introduced in\n",
|
||
"Chapter 3, is used.\n",
|
||
"The first and only required argument to url_for() is the endpoint name, the internal\n",
|
||
"name each route has. By default, the endpoint of a route is the name of the view func‐\n",
|
||
"tion attached to it. In this example, the view function that handles the root URL is\n",
|
||
"index(), so the name given to url_for() is index.\n",
|
||
"The last change is in the render_template() function, which now obtains the name\n",
|
||
"argument directly from the session using session.get('name'). As with regular dic‐\n",
|
||
"tionaries, using get() to request a dictionary key avoids an exception for keys that\n",
|
||
"aren’t found. The get() method returns a default value of None for a missing key.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 4b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"With this version of the application, you can see that refreshing the page in your\n",
|
||
"browser always results in the expected behavior.\n",
|
||
"Message Flashing\n",
|
||
"Sometimes it is useful to give the user a status update after a request is completed.\n",
|
||
"This could be a confirmation message, a warning, or an error. A typical example is\n",
|
||
"when you submit a login form to a website with a mistake and the server responds by\n",
|
||
"rendering the login form again with a message above it that informs you that your\n",
|
||
"username or password is invalid.\n",
|
||
"Flask includes this functionality as a core feature. Example 4-6 shows how the\n",
|
||
"flash() function can be used for this purpose.\n",
|
||
"Message Flashing \n",
|
||
"| \n",
|
||
"53\n",
|
||
"\n",
|
||
"Example 4-6. hello.py: flashed messages\n",
|
||
"from flask import Flask, render_template, session, redirect, url_for, flash\n",
|
||
"@app.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" form = NameForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" old_name = session.get('name')\n",
|
||
" if old_name is not None and old_name != form.name.data:\n",
|
||
" flash('Looks like you have changed your name!')\n",
|
||
" session['name'] = form.name.data\n",
|
||
" return redirect(url_for('index'))\n",
|
||
" return render_template('index.html',\n",
|
||
" form = form, name = session.get('name'))\n",
|
||
"In this example, each time a name is submitted it is compared against the name\n",
|
||
"stored in the user session, which will have been put there during a previous submis‐\n",
|
||
"sion of the same form. If the two names are different, the flash() function is invoked\n",
|
||
"with a message to be displayed on the next response sent back to the client.\n",
|
||
"Calling flash() is not enough to get messages displayed; the templates used by the\n",
|
||
"application need to render these messages. The best place to render flashed messages\n",
|
||
"is the base template, because that will enable these messages in all pages. Flask makes\n",
|
||
"a get_flashed_messages() function available to templates to retrieve the messages\n",
|
||
"and render them, as shown in Example 4-7.\n",
|
||
"Example 4-7. templates/base.html: rendering of flashed messages\n",
|
||
"{% block content %}\n",
|
||
"<div class=\"container\">\n",
|
||
" {% for message in get_flashed_messages() %}\n",
|
||
" <div class=\"alert alert-warning\">\n",
|
||
" <button type=\"button\" class=\"close\" data-dismiss=\"alert\">×</button>\n",
|
||
" {{ message }}\n",
|
||
" </div>\n",
|
||
" {% endfor %}\n",
|
||
" {% block page_content %}{% endblock %}\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"In this example, messages are rendered using Bootstrap’s alert CSS styles for warning\n",
|
||
"messages (one is shown in Figure 4-4).\n",
|
||
"54 \n",
|
||
"| \n",
|
||
"Chapter 4: Web Forms\n",
|
||
"\n",
|
||
"Figure 4-4. Flashed message\n",
|
||
"A loop is used because there could be multiple messages queued for display, one for\n",
|
||
"each time flash() was called in the previous request cycle. Messages that are\n",
|
||
"retrieved from get_flashed_messages() will not be returned the next time this func‐\n",
|
||
"tion is called, so flashed messages appear only once and are then discarded.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 4c to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Being able to accept data from the user through web forms is a feature required by\n",
|
||
"most applications, and so is the ability to store that data in permanent storage. Using\n",
|
||
"databases with Flask is the topic of the next chapter.\n",
|
||
"Message Flashing \n",
|
||
"| \n",
|
||
"55\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 5\n",
|
||
"Databases\n",
|
||
"A database stores application data in an organized way. The application then issues\n",
|
||
"queries to retrieve specific portions of the data as they are needed. The most com‐\n",
|
||
"monly used databases for web applications are those based on the relational model,\n",
|
||
"also called SQL databases in reference to the Structured Query Language they use.\n",
|
||
"But in recent years document-oriented and key-value databases, informally known\n",
|
||
"together as NoSQL databases, have become popular alternatives.\n",
|
||
"SQL Databases\n",
|
||
"Relational databases store data in tables, which model the different entities in the\n",
|
||
"application’s domain. For example, a database for an order management application\n",
|
||
"will likely have customers, products, and orders tables.\n",
|
||
"A table has a fixed number of columns and a variable number of rows. The columns\n",
|
||
"define the data attributes of the entity represented by the table. For example, a\n",
|
||
"customers table will have columns such as name, address, phone, and so on. Each\n",
|
||
"row in a table defines an actual data element that assigns values to some or all the\n",
|
||
"columns.\n",
|
||
"Tables have a special column called the primary key, which holds a unique identifier\n",
|
||
"for each row stored in the table. Tables can also have columns called foreign keys,\n",
|
||
"which reference the primary key of a row in the same or another table. These links\n",
|
||
"between rows are called relationships and are the foundation of the relational database\n",
|
||
"model.\n",
|
||
"Figure 5-1 shows a diagram of a simple database with two tables that store users and\n",
|
||
"user roles. The line that connects the two tables represents a relationship between the\n",
|
||
"tables.\n",
|
||
"57\n",
|
||
"\n",
|
||
"Figure 5-1. Relational database example\n",
|
||
"This graphical style of representing the structure of a database is called an entity-\n",
|
||
"relationship diagram. In this representation, boxes represent database tables, showing\n",
|
||
"lists of the table’s attributes or columns. The roles table stores the list of all possible\n",
|
||
"user roles, each identified by a unique id value—the table’s primary key. The users\n",
|
||
"table contains the list of users, each with its own unique id as well. Besides the id\n",
|
||
"primary keys, the roles table has a name column and the users table has username\n",
|
||
"and password columns.\n",
|
||
"The role_id column in the users table is a foreign key. The line that connects the\n",
|
||
"roles.id and users.role_id columns represents a relationship between the two\n",
|
||
"tables. The symbols attached to the line at each end indicate the cardinality of the\n",
|
||
"relationship. On the roles.id side, the line is shown to have a “one,” while on the\n",
|
||
"users.role_id side a “many” is represented. This depicts a one-to-many relationship,\n",
|
||
"indicating that each row from the roles table can be associated with many rows from\n",
|
||
"the users table.\n",
|
||
"As seen in the example, relational databases store data efficiently and avoid duplica‐\n",
|
||
"tion. Renaming a user role in this database is simple because role names exist in a\n",
|
||
"single place. Immediately after a role name is changed in the roles table, all users\n",
|
||
"that have a role_id that references the changed role will see the update.\n",
|
||
"On the other hand, having the data split into multiple tables can be a complication.\n",
|
||
"Producing a listing of users with their roles presents a small problem, because users\n",
|
||
"and user roles need to be read from two tables and joined before they can be presen‐\n",
|
||
"ted together. Relational database engines provide the support to perform join opera‐\n",
|
||
"tions between tables when necessary.\n",
|
||
"NoSQL Databases\n",
|
||
"Databases that do not follow the relational model described in the previous section\n",
|
||
"are collectively referred to as NoSQL databases. One common organization for\n",
|
||
"NoSQL databases uses collections instead of tables and documents instead of records.\n",
|
||
"NoSQL databases are designed in a way that makes joins difficult, so most of them do\n",
|
||
"not support this operation at all. For a NoSQL database structured as in Figure 5-1,\n",
|
||
"58 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"listing the users with their roles requires the application itself to perform the join\n",
|
||
"operation by reading the role_id field of each user and then searching the roles\n",
|
||
"table for it.\n",
|
||
"A more appropriate design for a NoSQL database is shown in Figure 5-2. This is the\n",
|
||
"result of applying an operation called denormalization, which reduces the number of\n",
|
||
"tables at the expense of data duplication.\n",
|
||
"Figure 5-2. NoSQL database example\n",
|
||
"A database with this structure has the role name explicitly stored with each user.\n",
|
||
"Renaming a role can then turn out to be an expensive operation that may require\n",
|
||
"updating a large number of documents.\n",
|
||
"But it isn’t all bad news with NoSQL databases. Having the data duplicated allows for\n",
|
||
"faster querying. Listing users and their roles is straightforward because no joins are\n",
|
||
"needed.\n",
|
||
"SQL or NoSQL?\n",
|
||
"SQL databases excel at storing structured data in an efficient and compact form.\n",
|
||
"These databases go to great lengths to preserve consistency, even in the face of power\n",
|
||
"failures or hardware malfunctions. The paradigm that allows relational databases to\n",
|
||
"reach this high level of reliability is called ACID, which stands for Atomicity, Consis‐\n",
|
||
"tency, Isolation, and Durability. NoSQL databases relax some of the ACID require‐\n",
|
||
"ments and as a result can sometimes get a performance edge.\n",
|
||
"A full analysis and comparison of database types is outside the scope of this book. For\n",
|
||
"small to medium-sized applications, both SQL and NoSQL databases are perfectly\n",
|
||
"capable and have practically equivalent performance.\n",
|
||
"Python Database Frameworks\n",
|
||
"Python has packages for most database engines, both open source and commercial.\n",
|
||
"Flask puts no restrictions on what database packages can be used, so you can work\n",
|
||
"with MySQL, Postgres, SQLite, Redis, MongoDB, CouchDB, or DynamoDB if any of\n",
|
||
"these is your favorite.\n",
|
||
"SQL or NoSQL? \n",
|
||
"| \n",
|
||
"59\n",
|
||
"\n",
|
||
"As if those weren’t enough choices, there are also a number of database abstraction\n",
|
||
"layer packages, such as SQLAlchemy or MongoEngine, that allow you to work at a\n",
|
||
"higher level with regular Python objects instead of database entities such as tables,\n",
|
||
"documents, or query languages.\n",
|
||
"There are a number of factors to evaluate when choosing a database framework:\n",
|
||
"Ease of use\n",
|
||
"When comparing straight database engines to database abstraction layers, the\n",
|
||
"second group clearly wins. Abstraction layers, also called object-relational map‐\n",
|
||
"pers (ORMs) or object-document mappers (ODMs), provide transparent conver‐\n",
|
||
"sion of high-level object-oriented operations into low-level database instructions.\n",
|
||
"Performance\n",
|
||
"The conversions that ORMs and ODMs have to do to translate from the object\n",
|
||
"domain into the database domain have an overhead. In most cases, the perfor‐\n",
|
||
"mance penalty is negligible, but it may not always be. In general, the productivity\n",
|
||
"gain obtained with ORMs and ODMs far outweighs a minimal performance deg‐\n",
|
||
"radation, so this isn’t a valid argument to drop ORMs and ODMs completely.\n",
|
||
"What makes sense is to choose a database abstraction layer that provides optional\n",
|
||
"access to the underlying database in case specific operations need to be optimized\n",
|
||
"by implementing them directly as native database instructions.\n",
|
||
"Portability\n",
|
||
"The database choices available on your development and production platforms\n",
|
||
"must be considered. For example, if you plan to host your application on a cloud\n",
|
||
"platform, then you should find out what database choices this service offers.\n",
|
||
"Another portability aspect applies to ORMs and ODMs. Although some of these\n",
|
||
"frameworks provide an abstraction layer for a single database engine, others\n",
|
||
"abstract even higher and provide a choice of database engines—all accessible\n",
|
||
"with the same object-oriented interface. The best example of this is the SQL‐\n",
|
||
"Alchemy ORM, which supports a list of relational database engines including the\n",
|
||
"popular MySQL, Postgres, and SQLite.\n",
|
||
"Flask integration\n",
|
||
"Choosing a framework that has integration with Flask is not absolutely required,\n",
|
||
"but it will save you from having to write the integration code yourself. Flask inte‐\n",
|
||
"gration could simplify configuration and operation, so using a package specifi‐\n",
|
||
"cally designed as a Flask extension should be preferred.\n",
|
||
"Based on these goals, the chosen database framework for the examples in this book\n",
|
||
"will be Flask-SQLAlchemy, the Flask extension wrapper for SQLAlchemy.\n",
|
||
"60 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"Database Management with Flask-SQLAlchemy\n",
|
||
"Flask-SQLAlchemy is a Flask extension that simplifies the use of SQLAlchemy inside\n",
|
||
"Flask applications. SQLAlchemy is a powerful relational database framework that\n",
|
||
"supports several database backends. It offers a high-level ORM and low-level access\n",
|
||
"to the database’s native SQL functionality.\n",
|
||
"Like most other extensions, Flask-SQLAlchemy is installed with pip:\n",
|
||
"(venv) $ pip install flask-sqlalchemy\n",
|
||
"In Flask-SQLAlchemy, a database is specified as a URL. Table 5-1 lists the format of\n",
|
||
"the URLs for the three most popular database engines.\n",
|
||
"Table 5-1. Flask-SQLAlchemy database URLs\n",
|
||
"Database engine\n",
|
||
"URL\n",
|
||
"MySQL\n",
|
||
"mysql://username:password@hostname/database\n",
|
||
"Postgres\n",
|
||
"postgresql://username:password@hostname/database\n",
|
||
"SQLite (Linux, macOS) sqlite:////absolute/path/to/database\n",
|
||
"SQLite (Windows)\n",
|
||
"sqlite:///c:/absolute/path/to/database\n",
|
||
"In these URLs, hostname refers to the server that hosts the database service, which\n",
|
||
"could be localhost or a remote server. Database servers can host several databases, so\n",
|
||
"database indicates the name of the database to use. For databases that need authenti‐\n",
|
||
"cation, username and password are the database user credentials.\n",
|
||
"SQLite databases do not have a server, so hostname, username, and\n",
|
||
"password are omitted and database is the filename on disk for the\n",
|
||
"database.\n",
|
||
"The URL of the application database must be configured as the key\n",
|
||
"SQLALCHEMY_DATABASE_URI in the Flask configuration object. The Flask-SQLAlchemy\n",
|
||
"documentation also suggests setting key SQLALCHEMY_TRACK_MODIFICATIONS to False\n",
|
||
"to use less memory unless signals for object changes are needed. Consult the Flask-\n",
|
||
"SQLAlchemy documentation for information on other configuration options.\n",
|
||
"Example 5-1 shows how to initialize and configure a simple SQLite database.\n",
|
||
"Example 5-1. hello.py: database configuration\n",
|
||
"import os\n",
|
||
"from flask_sqlalchemy import SQLAlchemy\n",
|
||
"basedir = os.path.abspath(os.path.dirname(__file__))\n",
|
||
"Database Management with Flask-SQLAlchemy \n",
|
||
"| \n",
|
||
"61\n",
|
||
"\n",
|
||
"app = Flask(__name__)\n",
|
||
"app.config['SQLALCHEMY_DATABASE_URI'] =\\\n",
|
||
" 'sqlite:///' + os.path.join(basedir, 'data.sqlite')\n",
|
||
"app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False\n",
|
||
"db = SQLAlchemy(app)\n",
|
||
"The db object instantiated from the class SQLAlchemy represents the database and\n",
|
||
"provides access to all the functionality of Flask-SQLAlchemy.\n",
|
||
"Model Definition\n",
|
||
"The term model is used when referring to the persistent entities used by the applica‐\n",
|
||
"tion. In the context of an ORM, a model is typically a Python class with attributes that\n",
|
||
"match the columns of a corresponding database table.\n",
|
||
"The database instance from Flask-SQLAlchemy provides a base class for models as\n",
|
||
"well as a set of helper classes and functions that are used to define their structure. The\n",
|
||
"roles and users tables from Figure 5-1 can be defined as the models Role and User\n",
|
||
"as shown in Example 5-2.\n",
|
||
"Example 5-2. hello.py: Role and User model definition\n",
|
||
"class Role(db.Model):\n",
|
||
" __tablename__ = 'roles'\n",
|
||
" id = db.Column(db.Integer, primary_key=True)\n",
|
||
" name = db.Column(db.String(64), unique=True)\n",
|
||
" def __repr__(self):\n",
|
||
" return '<Role %r>' % self.name\n",
|
||
"class User(db.Model):\n",
|
||
" __tablename__ = 'users'\n",
|
||
" id = db.Column(db.Integer, primary_key=True)\n",
|
||
" username = db.Column(db.String(64), unique=True, index=True)\n",
|
||
" def __repr__(self):\n",
|
||
" return '<User %r>' % self.username\n",
|
||
"The __tablename__ class variable defines the name of the table in the database. Flask-\n",
|
||
"SQLAlchemy assigns a default table name if __tablename__ is omitted, but those\n",
|
||
"default names do not follow the popular convention of using plurals for table names,\n",
|
||
"so it is best to name tables explicitly. The remaining class variables are the attributes\n",
|
||
"of the model, defined as instances of the db.Column class.\n",
|
||
"62 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"The first argument given to the db.Column constructor is the type of the database col‐\n",
|
||
"umn and model attribute. Table 5-2 lists some of the column types that are available,\n",
|
||
"along with the Python types used in the model.\n",
|
||
"Table 5-2. Most common SQLAlchemy column types\n",
|
||
"Type name\n",
|
||
"Python type\n",
|
||
"Description\n",
|
||
"Integer\n",
|
||
"int\n",
|
||
"Regular integer, typically 32 bits\n",
|
||
"SmallInteger\n",
|
||
"int\n",
|
||
"Short-range integer, typically 16 bits\n",
|
||
"BigInteger\n",
|
||
"int or long\n",
|
||
"Unlimited precision integer\n",
|
||
"Float\n",
|
||
"float\n",
|
||
"Floating-point number\n",
|
||
"Numeric\n",
|
||
"decimal.Decimal\n",
|
||
"Fixed-point number\n",
|
||
"String\n",
|
||
"str\n",
|
||
"Variable-length string\n",
|
||
"Text\n",
|
||
"str\n",
|
||
"Variable-length string, optimized for large or unbounded length\n",
|
||
"Unicode\n",
|
||
"unicode\n",
|
||
"Variable-length Unicode string\n",
|
||
"UnicodeText\n",
|
||
"unicode\n",
|
||
"Variable-length Unicode string, optimized for large or unbounded length\n",
|
||
"Boolean\n",
|
||
"bool\n",
|
||
"Boolean value\n",
|
||
"Date\n",
|
||
"datetime.date\n",
|
||
"Date value\n",
|
||
"Time\n",
|
||
"datetime.time\n",
|
||
"Time value\n",
|
||
"DateTime\n",
|
||
"datetime.datetime\n",
|
||
"Date and time value\n",
|
||
"Interval\n",
|
||
"datetime.timedelta Time interval\n",
|
||
"Enum\n",
|
||
"str\n",
|
||
"List of string values\n",
|
||
"PickleType\n",
|
||
"Any Python object\n",
|
||
"Automatic Pickle serialization\n",
|
||
"LargeBinary\n",
|
||
"str\n",
|
||
"Binary blob\n",
|
||
"The remaining arguments to db.Column specify configuration options for each\n",
|
||
"attribute. Table 5-3 lists some of the options available.\n",
|
||
"Table 5-3. Most common SQLAlchemy column options\n",
|
||
"Option name\n",
|
||
"Description\n",
|
||
"primary_key\n",
|
||
"If set to True, the column is the table’s primary key.\n",
|
||
"unique\n",
|
||
"If set to True, do not allow duplicate values for this column.\n",
|
||
"index\n",
|
||
"If set to True, create an index for this column, so that queries are more efficient.\n",
|
||
"nullable\n",
|
||
"If set to True, allow empty values for this column. If set to False, the column will not allow null values.\n",
|
||
"default\n",
|
||
"Define a default value for the column.\n",
|
||
"Model Definition \n",
|
||
"| \n",
|
||
"63\n",
|
||
"\n",
|
||
"Flask-SQLAlchemy requires all models to define a primary key col‐\n",
|
||
"umn, which is commonly named id.\n",
|
||
"Although it’s not strictly necessary, the two models include a __repr__() method to\n",
|
||
"give them a readable string representation that can be used for debugging and testing\n",
|
||
"purposes.\n",
|
||
"Relationships\n",
|
||
"Relational databases establish connections between rows in different tables through\n",
|
||
"the use of relationships. The relational diagram in Figure 5-1 expresses a simple rela‐\n",
|
||
"tionship between users and their roles. This is a one-to-many relationship from roles\n",
|
||
"to users, because one role can belong to many users, but each user can have only one\n",
|
||
"role.\n",
|
||
"Example 5-3 shows how the one-to-many relationship in Figure 5-1 is represented in\n",
|
||
"the model classes.\n",
|
||
"Example 5-3. hello.py: relationships in the database models\n",
|
||
"class Role(db.Model):\n",
|
||
" # ...\n",
|
||
" users = db.relationship('User', backref='role')\n",
|
||
"class User(db.Model):\n",
|
||
" # ...\n",
|
||
" role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))\n",
|
||
"As seen in Figure 5-1, a relationship connects two rows through the use of a foreign\n",
|
||
"key. The role_id column added to the User model is defined as a foreign key, and\n",
|
||
"that establishes the relationship. The 'roles.id' argument to db.ForeignKey()\n",
|
||
"specifies that the column should be interpreted as having id values from rows in the\n",
|
||
"roles table.\n",
|
||
"The users attribute added to the model Role represents the object-oriented view of\n",
|
||
"the relationship, as seen from the “one” side. Given an instance of class Role, the\n",
|
||
"users attribute will return the list of users associated with that role (i.e., the “many”\n",
|
||
"side). The first argument to db.relationship() indicates what model is on the other\n",
|
||
"side of the relationship. The model class can be provided as a string if the class is\n",
|
||
"defined later in the module.\n",
|
||
"The backref argument to db.relationship() defines the reverse direction of the\n",
|
||
"relationship, by adding a role attribute to the User model. This attribute can be used\n",
|
||
"64 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"on any instance of User instead of the role_id foreign key to access the Role model\n",
|
||
"as an object.\n",
|
||
"In most cases db.relationship() can locate the relationship’s foreign key on its own,\n",
|
||
"but sometimes it cannot determine what column to use as a foreign key. For example,\n",
|
||
"if the User model had two or more columns defined as Role foreign keys, then\n",
|
||
"SQLAlchemy would not know which one of the two to use. Whenever the foreign key\n",
|
||
"configuration is ambiguous, additional arguments to db.relationship() need to be\n",
|
||
"given. Table 5-4 lists some of the common configuration options that can be used to\n",
|
||
"define a relationship.\n",
|
||
"Table 5-4. Common SQLAlchemy relationship options\n",
|
||
"Option name\n",
|
||
"Description\n",
|
||
"backref\n",
|
||
"Add a back reference in the other model in the relationship.\n",
|
||
"primaryjoin\n",
|
||
"Specify the join condition between the two models explicitly. This is necessary only for ambiguous\n",
|
||
"relationships.\n",
|
||
"lazy\n",
|
||
"Specify how the related items are to be loaded. Possible values are select (items are loaded on\n",
|
||
"demand the first time they are accessed), immediate (items are loaded when the source object is\n",
|
||
"loaded), joined (items are loaded immediately, but as a join), subquery (items are loaded\n",
|
||
"immediately, but as a subquery), noload (items are never loaded), and dynamic (instead of loading\n",
|
||
"the items, the query that can load them is given).\n",
|
||
"uselist\n",
|
||
"If set to False, use a scalar instead of a list.\n",
|
||
"order_by\n",
|
||
"Specify the ordering used for the items in the relationship.\n",
|
||
"secondary\n",
|
||
"Specify the name of the association table to use in many-to-many relationships.\n",
|
||
"secondaryjoin Specify the secondary join condition for many-to-many relationships when SQLAlchemy cannot\n",
|
||
"determine it on its own.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 5a to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"There are other relationship types besides one-to-many. The one-to-one relationship\n",
|
||
"can be expressed the same way as one-to-many, as described earlier, but with the\n",
|
||
"uselist option set to False within the db.relationship() definition so that the\n",
|
||
"“many” side becomes a “one” side. The many-to-one relationship can also be\n",
|
||
"expressed as a one-to-many if the tables are reversed, or it can be expressed with the\n",
|
||
"foreign key and the db.relationship() definition both on the “many” side. The\n",
|
||
"most complex relationship type, many-to-many, requires an additional table called an\n",
|
||
"association or junction table. You will learn about many-to-many relationships in\n",
|
||
"Chapter 12.\n",
|
||
"Relationships \n",
|
||
"| \n",
|
||
"65\n",
|
||
"\n",
|
||
"Database Operations\n",
|
||
"The models are now fully configured according to the database diagram in Figure 5-1\n",
|
||
"and are ready to be used. The best way to learn how to work with these models is in a\n",
|
||
"Python shell. The following sections will walk you through the most common data‐\n",
|
||
"base operations in a shell started with the flask shell command. Before you use\n",
|
||
"this command, make sure the FLASK_APP environment variable is set to hello.py, as\n",
|
||
"shown in Chapter 2.\n",
|
||
"Creating the Tables\n",
|
||
"The very first thing to do is to instruct Flask-SQLAlchemy to create a database based\n",
|
||
"on the model classes. The db.create_all() function locates all the subclasses of\n",
|
||
"db.Model and creates corresponding tables in the database for them:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> from hello import db\n",
|
||
">>> db.create_all()\n",
|
||
"If you check the application directory, you will now see a new file there called\n",
|
||
"data.sqlite, the name that was given to the SQLite database in the configuration. The\n",
|
||
"db.create_all() function will not re-create or update a database table if it already\n",
|
||
"exists in the database. This can be inconvenient when the models are modified and\n",
|
||
"the changes need to be applied to an existing database. The brute-force solution to\n",
|
||
"update existing database tables to a different schema is to remove the old tables first:\n",
|
||
">>> db.drop_all()\n",
|
||
">>> db.create_all()\n",
|
||
"Unfortunately, this method has the undesired side effect of destroying all the data in\n",
|
||
"the old database. A better solution to the problem of updating databases is presented\n",
|
||
"near the end of the chapter.\n",
|
||
"Inserting Rows\n",
|
||
"The following example creates a few roles and users:\n",
|
||
">>> from hello import Role, User\n",
|
||
">>> admin_role = Role(name='Admin')\n",
|
||
">>> mod_role = Role(name='Moderator')\n",
|
||
">>> user_role = Role(name='User')\n",
|
||
">>> user_john = User(username='john', role=admin_role)\n",
|
||
">>> user_susan = User(username='susan', role=user_role)\n",
|
||
">>> user_david = User(username='david', role=user_role)\n",
|
||
"The constructors for models accept initial values for the model attributes as keyword\n",
|
||
"arguments. Note that the role attribute can be used, even though it is not a real data‐\n",
|
||
"base column but a high-level representation of the one-to-many relationship. The id\n",
|
||
"66 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"attribute of these new objects is not set explicitly: the primary keys in many databases\n",
|
||
"are managed by the database itself. The objects exist only on the Python side so far;\n",
|
||
"they have not been written to the database yet. Because of that, their id values have\n",
|
||
"not yet been assigned:\n",
|
||
">>> print(admin_role.id)\n",
|
||
"None\n",
|
||
">>> print(mod_role.id)\n",
|
||
"None\n",
|
||
">>> print(user_role.id)\n",
|
||
"None\n",
|
||
"Changes to the database are managed through a database session, which Flask-\n",
|
||
"SQLAlchemy provides as db.session. To prepare objects to be written to the data‐\n",
|
||
"base, they must be added to the session:\n",
|
||
">>> db.session.add(admin_role)\n",
|
||
">>> db.session.add(mod_role)\n",
|
||
">>> db.session.add(user_role)\n",
|
||
">>> db.session.add(user_john)\n",
|
||
">>> db.session.add(user_susan)\n",
|
||
">>> db.session.add(user_david)\n",
|
||
"Or, more concisely:\n",
|
||
">>> db.session.add_all([admin_role, mod_role, user_role,\n",
|
||
"... user_john, user_susan, user_david])\n",
|
||
"To write the objects to the database, the session needs to be committed by calling its\n",
|
||
"commit() method:\n",
|
||
">>> db.session.commit()\n",
|
||
"Check the id attributes again after having the data committed to see that they are\n",
|
||
"now set:\n",
|
||
">>> print(admin_role.id)\n",
|
||
"1\n",
|
||
">>> print(mod_role.id)\n",
|
||
"2\n",
|
||
">>> print(user_role.id)\n",
|
||
"3\n",
|
||
"The db.session database session is not related to the Flask\n",
|
||
"session object discussed in Chapter 4. Database sessions are also\n",
|
||
"called transactions.\n",
|
||
"Database sessions are extremely useful in keeping the database consistent. The com‐\n",
|
||
"mit operation writes all the objects that were added to the session atomically. If an\n",
|
||
"Database Operations \n",
|
||
"| \n",
|
||
"67\n",
|
||
"\n",
|
||
"error occurs while the session is being written, the whole session is discarded. If you\n",
|
||
"always commit related changes together in a session, you are guaranteed to avoid\n",
|
||
"database inconsistencies due to partial updates.\n",
|
||
"A \n",
|
||
"database \n",
|
||
"session \n",
|
||
"can \n",
|
||
"also \n",
|
||
"be \n",
|
||
"rolled \n",
|
||
"back. \n",
|
||
"If\n",
|
||
"db.session.rollback() is called, any objects that were added to\n",
|
||
"the database session are restored to the state they have in the data‐\n",
|
||
"base.\n",
|
||
"Modifying Rows\n",
|
||
"The add() method of the database session can also be used to update models. Con‐\n",
|
||
"tinuing in the same shell session, the following example renames the \"Admin\" role to\n",
|
||
"\"Administrator\":\n",
|
||
">>> admin_role.name = 'Administrator'\n",
|
||
">>> db.session.add(admin_role)\n",
|
||
">>> db.session.commit()\n",
|
||
"Deleting Rows\n",
|
||
"The database session also has a delete() method. The following example deletes the\n",
|
||
"\"Moderator\" role from the database:\n",
|
||
">>> db.session.delete(mod_role)\n",
|
||
">>> db.session.commit()\n",
|
||
"Note that deletions, like insertions and updates, are executed only when the database\n",
|
||
"session is committed.\n",
|
||
"Querying Rows\n",
|
||
"Flask-SQLAlchemy makes a query object available in each model class. The most\n",
|
||
"basic query for a model is triggered with the all() method, which returns the entire\n",
|
||
"contents of the corresponding table:\n",
|
||
">>> Role.query.all()\n",
|
||
"[<Role 'Administrator'>, <Role 'User'>]\n",
|
||
">>> User.query.all()\n",
|
||
"[<User 'john'>, <User 'susan'>, <User 'david'>]\n",
|
||
"A query object can be configured to issue more specific database searches through\n",
|
||
"the use of filters. The following example finds all the users that were assigned the\n",
|
||
"\"User\" role:\n",
|
||
">>> User.query.filter_by(role=user_role).all()\n",
|
||
"[<User 'susan'>, <User 'david'>]\n",
|
||
"68 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"It is also possible to inspect the native SQL query that SQLAlchemy generates for a\n",
|
||
"given query by converting the query object to a string:\n",
|
||
">>> str(User.query.filter_by(role=user_role))\n",
|
||
"'SELECT users.id AS users_id, users.username AS users_username,\n",
|
||
"users.role_id AS users_role_id \\nFROM users \\nWHERE :param_1 = users.role_id'\n",
|
||
"If you exit the shell session, the objects created in the previous example will cease to\n",
|
||
"exist as Python objects but will continue to exist as rows in their respective database\n",
|
||
"tables. If you then start a brand-new shell session, you have to re-create the Python\n",
|
||
"objects from their database rows. The following example issues a query that loads the\n",
|
||
"user role with name \"User\":\n",
|
||
">>> user_role = Role.query.filter_by(name='User').first()\n",
|
||
"Note how in this case, the query was issued with the first() method instead of\n",
|
||
"all(). While all() returns all the results of the query as a list, first() returns only\n",
|
||
"the first result or None if there are no results, so it is a convenient method to use for\n",
|
||
"queries that are known to return one result at the most.\n",
|
||
"Filters such as filter_by() are invoked on a query object and return a new refined\n",
|
||
"query. Multiple filters can be called in sequence until the query is configured as\n",
|
||
"needed.\n",
|
||
"Table 5-5 shows some of the most common filters available to queries. The complete\n",
|
||
"list is in the SQLAlchemy documentation.\n",
|
||
"Table 5-5. Common SQLAlchemy query filters\n",
|
||
"Option\n",
|
||
"Description\n",
|
||
"filter()\n",
|
||
"Returns a new query that adds an additional filter to the original query\n",
|
||
"filter_by()\n",
|
||
"Returns a new query that adds an additional equality filter to the original query\n",
|
||
"limit()\n",
|
||
"Returns a new query that limits the number of results of the original query to the given number\n",
|
||
"offset()\n",
|
||
"Returns a new query that applies an offset into the list of results of the original query\n",
|
||
"order_by()\n",
|
||
"Returns a new query that sorts the results of the original query according to the given criteria\n",
|
||
"group_by()\n",
|
||
"Returns a new query that groups the results of the original query according to the given criteria\n",
|
||
"After the desired filters have been applied to the query, a call to all() will cause the\n",
|
||
"query to execute and return the results as a list—but there are other ways to trigger\n",
|
||
"the execution of a query besides all(). Table 5-6 shows other query execution meth‐\n",
|
||
"ods.\n",
|
||
"Database Operations \n",
|
||
"| \n",
|
||
"69\n",
|
||
"\n",
|
||
"Table 5-6. Most common SQLAlchemy query executors\n",
|
||
"Option\n",
|
||
"Description\n",
|
||
"all()\n",
|
||
"Returns all the results of a query as a list\n",
|
||
"first()\n",
|
||
"Returns the first result of a query, or None if there are no results\n",
|
||
"first_or_404() Returns the first result of a query, or aborts the request and sends a 404 error as the response if there\n",
|
||
"are no results\n",
|
||
"get()\n",
|
||
"Returns the row that matches the given primary key, or None if no matching row is found\n",
|
||
"get_or_404()\n",
|
||
"Returns the row that matches the given primary key or, if the key is not found, aborts the request and\n",
|
||
"sends a 404 error as the response\n",
|
||
"count()\n",
|
||
"Returns the result count of the query\n",
|
||
"paginate()\n",
|
||
"Returns a Pagination object that contains the specified range of results\n",
|
||
"Relationships work similarly to queries. The following example queries the one-to-\n",
|
||
"many relationship between roles and users from both ends:\n",
|
||
">>> users = user_role.users\n",
|
||
">>> users\n",
|
||
"[<User 'susan'>, <User 'david'>]\n",
|
||
">>> users[0].role\n",
|
||
"<Role 'User'>\n",
|
||
"The user_role.users query here has a small problem. The implicit query that runs\n",
|
||
"when the user_role.users expression is issued internally calls all() to return the\n",
|
||
"list of users. Because the query object is hidden, it is not possible to refine it with\n",
|
||
"additional query filters. In this particular example, it may have been useful to request\n",
|
||
"that the user list be returned in alphabetical order. In Example 5-4, the configuration\n",
|
||
"of the relationship is modified with a lazy='dynamic' argument to request that the\n",
|
||
"query is not automatically executed.\n",
|
||
"Example 5-4. hello.py: dynamic database relationships\n",
|
||
"class Role(db.Model):\n",
|
||
" # ...\n",
|
||
" users = db.relationship('User', backref='role', lazy='dynamic')\n",
|
||
" # ...\n",
|
||
"With the relationship configured in this way, user_role.users returns a query that\n",
|
||
"hasn’t executed yet, so filters can be added to it:\n",
|
||
">>> user_role.users.order_by(User.username).all()\n",
|
||
"[<User 'david'>, <User 'susan'>]\n",
|
||
">>> user_role.users.count()\n",
|
||
"2\n",
|
||
"70 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"Database Use in View Functions\n",
|
||
"The database operations described in the previous sections can be used directly inside\n",
|
||
"view functions. Example 5-5 shows a new version of the home page route that records\n",
|
||
"names entered by users in the database.\n",
|
||
"Example 5-5. hello.py: database use in view functions\n",
|
||
"@app.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" form = NameForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" user = User.query.filter_by(username=form.name.data).first()\n",
|
||
" if user is None:\n",
|
||
" user = User(username=form.name.data)\n",
|
||
" db.session.add(user)\n",
|
||
" db.session.commit()\n",
|
||
" session['known'] = False\n",
|
||
" else:\n",
|
||
" session['known'] = True\n",
|
||
" session['name'] = form.name.data\n",
|
||
" form.name.data = ''\n",
|
||
" return redirect(url_for('index'))\n",
|
||
" return render_template('index.html',\n",
|
||
" form=form, name=session.get('name'),\n",
|
||
" known=session.get('known', False))\n",
|
||
"In this modified version of the application, each time a name is submitted the appli‐\n",
|
||
"cation checks for it in the database using the filter_by() query filter. A known vari‐\n",
|
||
"able is written to the user session so that after the redirect the information can be sent\n",
|
||
"to the template, where it is used to customize the greeting. Note that for the applica‐\n",
|
||
"tion to work, the database tables must be created in a Python shell as shown earlier.\n",
|
||
"The new version of the associated template is shown in Example 5-6. This template\n",
|
||
"uses the known argument to add a second line to the greeting that is different for\n",
|
||
"known and new users.\n",
|
||
"Example 5-6. templates/index.html: customized greeting in template\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% import \"bootstrap/wtf.html\" as wtf %}\n",
|
||
"{% block title %}Flasky{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>\n",
|
||
" {% if not known %}\n",
|
||
"Database Use in View Functions \n",
|
||
"| \n",
|
||
"71\n",
|
||
"\n",
|
||
" <p>Pleased to meet you!</p>\n",
|
||
" {% else %}\n",
|
||
" <p>Happy to see you again!</p>\n",
|
||
" {% endif %}\n",
|
||
"</div>\n",
|
||
"{{ wtf.quick_form(form) }}\n",
|
||
"{% endblock %}\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 5b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Integration with the Python Shell\n",
|
||
"Having to import the database instance and the models each time a shell session is\n",
|
||
"started is tedious work. To avoid having to constantly repeat these steps, the flask\n",
|
||
"shell command can be configured to automatically import these objects.\n",
|
||
"To add objects to the import list, a shell context processor must be created and regis‐\n",
|
||
"tered with the app.shell_context_processor decorator. This is shown in\n",
|
||
"Example 5-7.\n",
|
||
"Example 5-7. hello.py: adding a shell context\n",
|
||
"@app.shell_context_processor\n",
|
||
"def make_shell_context():\n",
|
||
" return dict(db=db, User=User, Role=Role)\n",
|
||
"The shell context processor function returns a dictionary that includes the database\n",
|
||
"instance and the models. The flask shell command will import these items auto‐\n",
|
||
"matically into the shell, in addition to app, which is imported by default:\n",
|
||
"$ flask shell\n",
|
||
">>> app\n",
|
||
"<Flask 'hello'>\n",
|
||
">>> db\n",
|
||
"<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>\n",
|
||
">>> User\n",
|
||
"<class 'hello.User'>\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 5c to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"72 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"Database Migrations with Flask-Migrate\n",
|
||
"As you make progress developing an application, you will find that your database\n",
|
||
"models need to change, and when that happens the database needs to be updated as\n",
|
||
"well. Flask-SQLAlchemy creates database tables from models only when they do not\n",
|
||
"exist already, so the only way to make it update tables is by destroying the old tables\n",
|
||
"first—but of course, this causes all the data in the database to be lost.\n",
|
||
"A better solution is to use a database migration framework. In the same way source\n",
|
||
"code version control tools keep track of changes to source code files, a database\n",
|
||
"migration framework keeps track of changes to a database schema, allowing incre‐\n",
|
||
"mental changes to be applied.\n",
|
||
"The developer of SQLAlchemy has written a migration framework called Alembic,\n",
|
||
"but instead of using Alembic directly, Flask applications can use the Flask-Migrate\n",
|
||
"extension, a lightweight Alembic wrapper that integrates it with the flask command.\n",
|
||
"Creating a Migration Repository\n",
|
||
"To begin, Flask-Migrate must be installed in the virtual environment:\n",
|
||
"(venv) $ pip install flask-migrate\n",
|
||
"Example 5-8 shows how the extension is initialized.\n",
|
||
"Example 5-8. hello.py: Flask-Migrate initialization\n",
|
||
"from flask_migrate import Migrate\n",
|
||
"# ...\n",
|
||
"migrate = Migrate(app, db)\n",
|
||
"To expose the database migration commands, Flask-Migrate adds a flask db com‐\n",
|
||
"mand with several subcommands. When you work on a new project, you can add\n",
|
||
"support for database migrations with the init subcommand:\n",
|
||
"(venv) $ flask db init\n",
|
||
" Creating directory /home/flask/flasky/migrations...done\n",
|
||
" Creating directory /home/flask/flasky/migrations/versions...done\n",
|
||
" Generating /home/flask/flasky/migrations/alembic.ini...done\n",
|
||
" Generating /home/flask/flasky/migrations/env.py...done\n",
|
||
" Generating /home/flask/flasky/migrations/env.pyc...done\n",
|
||
" Generating /home/flask/flasky/migrations/README...done\n",
|
||
" Generating /home/flask/flasky/migrations/script.py.mako...done\n",
|
||
" Please edit configuration/connection/logging settings in\n",
|
||
" '/home/flask/flasky/migrations/alembic.ini' before proceeding.\n",
|
||
"Database Migrations with Flask-Migrate \n",
|
||
"| \n",
|
||
"73\n",
|
||
"\n",
|
||
"This command creates a migrations directory, where all the migration scripts will be\n",
|
||
"stored. If you are following the example project using git checkout, you do not need\n",
|
||
"to do this step, as the migration repository is already included in the GitHub reposi‐\n",
|
||
"tory.\n",
|
||
"The files in a database migration repository must always be added\n",
|
||
"to version control along with the rest of the application.\n",
|
||
"Creating a Migration Script\n",
|
||
"In Alembic, a database migration is represented by a migration script. This script has\n",
|
||
"two functions called upgrade() and downgrade(). The upgrade() function applies\n",
|
||
"the database changes that are part of the migration, and the downgrade() function\n",
|
||
"removes them. This ability to add and remove changes means, Alembic can reconfig‐\n",
|
||
"ure a database to any point in the change history.\n",
|
||
"Alembic migrations can be created manually or automatically using the revision and\n",
|
||
"migrate commands, respectively. A manual migration creates a migration skeleton\n",
|
||
"script with empty upgrade() and downgrade() functions that need to be imple‐\n",
|
||
"mented by the developer using directives exposed by Alembic’s Operations object.\n",
|
||
"An automatic migration attempts to generate the code for the upgrade() and\n",
|
||
"downgrade() functions by looking for differences between the model definitions and\n",
|
||
"the current state of the database.\n",
|
||
"Automatic migrations are not always accurate and can miss some\n",
|
||
"details that are ambiguous. For example, if a column is renamed, an\n",
|
||
"automatically generated migration may show that the column in\n",
|
||
"question was deleted and a new column was added with the new\n",
|
||
"name. Leaving the migration as is will cause the data in this column\n",
|
||
"to be lost! For this reason, migration scripts generated automati‐\n",
|
||
"cally should always be reviewed and manually corrected if they\n",
|
||
"have any inaccuracies.\n",
|
||
"To make changes to your database schema with Flask-Migrate, the following proce‐\n",
|
||
"dure needs to be followed:\n",
|
||
"1. Make the necessary changes to the model classes.\n",
|
||
"2. Create an automatic migration script with the flask db migrate command.\n",
|
||
"3. Review the generated script and adjust it so that it accurately represents the\n",
|
||
"changes that were made to the models.\n",
|
||
"74 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"4. Add the migration script to source control.\n",
|
||
"5. Apply the migration to the database with the flask db upgrade command.\n",
|
||
"The flask db migrate subcommand creates an automatic migration script:\n",
|
||
"(venv) $ flask db migrate -m \"initial migration\"\n",
|
||
"INFO [alembic.migration] Context impl SQLiteImpl.\n",
|
||
"INFO [alembic.migration] Will assume non-transactional DDL.\n",
|
||
"INFO [alembic.autogenerate] Detected added table 'roles'\n",
|
||
"INFO [alembic.autogenerate] Detected added table 'users'\n",
|
||
"INFO [alembic.autogenerate.compare] Detected added index\n",
|
||
"'ix_users_username' on '['username']'\n",
|
||
" Generating /home/flask/flasky/migrations/versions/1bc\n",
|
||
" 594146bb5_initial_migration.py...done\n",
|
||
"If you are following the git checkout instructions to incrementally update the exam‐\n",
|
||
"ple application, you do not need to issue the migrate commands, as the migration\n",
|
||
"scripts are already incorporated into the Git repository tags.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 5d to check out this version of the applica‐\n",
|
||
"tion. Note that you do not need to generate the migration reposi‐\n",
|
||
"tory and the migration scripts for this application as these are\n",
|
||
"included in the GitHub repository.\n",
|
||
"Upgrading the Database\n",
|
||
"Once a migration script has been reviewed and accepted, it can be applied to the data‐\n",
|
||
"base using the flask db upgrade command:\n",
|
||
"(venv) $ flask db upgrade\n",
|
||
"INFO [alembic.migration] Context impl SQLiteImpl.\n",
|
||
"INFO [alembic.migration] Will assume non-transactional DDL.\n",
|
||
"INFO [alembic.migration] Running upgrade None -> 1bc594146bb5, initial migration\n",
|
||
"For a first migration, this is effectively equivalent to calling db.create_all(), but in\n",
|
||
"successive migrations the flask db upgrade command applies updates to the tables\n",
|
||
"without affecting their contents.\n",
|
||
"Database Migrations with Flask-Migrate \n",
|
||
"| \n",
|
||
"75\n",
|
||
"\n",
|
||
"If you have been working with the application in its previous\n",
|
||
"stages, you already have a database file that was created with the\n",
|
||
"db.create_all() function earlier. In this state, the flask db\n",
|
||
"upgrade will fail because it will try to create database tables that\n",
|
||
"already exist. A simple way to address this problem is to delete\n",
|
||
"your data.sqlite database file and then run flask db upgrade to\n",
|
||
"generate a new database through the migration framework.\n",
|
||
"Another option is to skip the flask db upgrade and instead mark\n",
|
||
"the existing database as upgraded using the flask db stamp com‐\n",
|
||
"mand.\n",
|
||
"Adding More Migrations\n",
|
||
"As you work on your own projects, you are going to find that you need to make\n",
|
||
"changes to your database models very often. When you manage the database through\n",
|
||
"a migration framework, all changes must be defined in migration scripts, because\n",
|
||
"anything that is not tracked in a migration will not be repeatable. The procedure to\n",
|
||
"introduce a change in the database is similar to what was done to introduce the first\n",
|
||
"migration:\n",
|
||
"1. Make the necessary changes in the database models.\n",
|
||
"2. Generate a migration with the flask db migrate command.\n",
|
||
"3. Review the generated migration script and correct it if it has any inaccuracies.\n",
|
||
"4. Apply the changes to the database with the flask db upgrade command.\n",
|
||
"While working on a specific feature, you may find that you need to make several\n",
|
||
"changes to your database models before you get them the way you want them. If your\n",
|
||
"last migration has not been committed to source control yet, you can opt to expand it\n",
|
||
"to incorporate new changes as you make them, and this will save you from having\n",
|
||
"lots of very small migration scripts that are meaningless on their own. The procedure\n",
|
||
"to expand the last migration script is as follows:\n",
|
||
"1. Remove the last migration from the database with the flask db downgrade com‐\n",
|
||
"mand (note that this may cause some data to be lost).\n",
|
||
"2. Delete the last migration script, which is now orphaned.\n",
|
||
"3. Generate a new database migration with the flask db migrate command,\n",
|
||
"which will now include the changes in the migration script you just removed,\n",
|
||
"plus any other changes you’ve made to the models.\n",
|
||
"4. Review and apply the migration script as described previously.\n",
|
||
"76 \n",
|
||
"| \n",
|
||
"Chapter 5: Databases\n",
|
||
"\n",
|
||
"Consult the Flask-Migrate documentation to learn about other\n",
|
||
"subcommands related to database migrations.\n",
|
||
"The topic of database design and usage is very important; entire books have been\n",
|
||
"written on the subject. You should consider this chapter as an overview; more\n",
|
||
"advanced topics will be discussed in later chapters. The next chapter is dedicated to\n",
|
||
"sending email.\n",
|
||
"Database Migrations with Flask-Migrate \n",
|
||
"| \n",
|
||
"77\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 6\n",
|
||
"Email\n",
|
||
"Many types of applications need to notify users when certain events occur, and the\n",
|
||
"usual method of communication is email. In this chapter you are going to learn how\n",
|
||
"to send emails from a Flask application.\n",
|
||
"Email Support with Flask-Mail\n",
|
||
"Although the smtplib package from the Python standard library can be used to send\n",
|
||
"email inside a Flask application, the Flask-Mail extension wraps smtplib and integra‐\n",
|
||
"tes it nicely with Flask. Flask-Mail is installed with pip:\n",
|
||
"(venv) $ pip install flask-mail\n",
|
||
"The extension connects to a Simple Mail Transfer Protocol (SMTP) server and passes\n",
|
||
"emails to it for delivery. If no configuration is given, Flask-Mail connects to localhost\n",
|
||
"at port 25 and sends email without authentication. Table 6-1 shows the list of configu‐\n",
|
||
"ration keys that can be used to configure the SMTP server.\n",
|
||
"Table 6-1. Flask-Mail SMTP server configuration keys\n",
|
||
"Key\n",
|
||
"Default\n",
|
||
"Description\n",
|
||
"MAIL_SERVER\n",
|
||
"localhost\n",
|
||
"Hostname or IP address of the email server\n",
|
||
"MAIL_PORT\n",
|
||
"25\n",
|
||
"Port of the email server\n",
|
||
"MAIL_USE_TLS\n",
|
||
"False\n",
|
||
"Enable Transport Layer Security (TLS) security\n",
|
||
"MAIL_USE_SSL\n",
|
||
"False\n",
|
||
"Enable Secure Sockets Layer (SSL) security\n",
|
||
"MAIL_USERNAME\n",
|
||
"None\n",
|
||
"Mail account username\n",
|
||
"MAIL_PASSWORD\n",
|
||
"None\n",
|
||
"Mail account password\n",
|
||
"79\n",
|
||
"\n",
|
||
"During development it may be more convenient to connect to an external SMTP\n",
|
||
"server. As an example, Example 6-1 shows how to configure the application to send\n",
|
||
"email through a Google Gmail account.\n",
|
||
"Example 6-1. hello.py: Flask-Mail configuration for Gmail\n",
|
||
"import os\n",
|
||
"# ...\n",
|
||
"app.config['MAIL_SERVER'] = 'smtp.googlemail.com'\n",
|
||
"app.config['MAIL_PORT'] = 587\n",
|
||
"app.config['MAIL_USE_TLS'] = True\n",
|
||
"app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')\n",
|
||
"app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')\n",
|
||
"Never write account credentials directly in your scripts, particularly\n",
|
||
"if you plan to release your work as open source. To protect your\n",
|
||
"account information, have your script import sensitive information\n",
|
||
"from environment variables.\n",
|
||
"For security reasons, Gmail accounts are configured to require\n",
|
||
"external applications to use OAuth2 authentication to connect to\n",
|
||
"the email server. Unfortunately, Python’s smtplib library does not\n",
|
||
"support this method of authentication. To make your Gmail\n",
|
||
"account accept standard SMTP authentication, go to your Google\n",
|
||
"account settings page and select “Signing in to Google” from the\n",
|
||
"left menu bar. On that page, locate the “Allow less secure apps” set‐\n",
|
||
"ting and make sure it is enabled. If enabling this setting on your\n",
|
||
"personal Gmail account concerns you, create a secondary account\n",
|
||
"only to test sending emails.\n",
|
||
"Flask-Mail is initialized as shown in Example 6-2.\n",
|
||
"Example 6-2. hello.py: Flask-Mail initialization\n",
|
||
"from flask_mail import Mail\n",
|
||
"mail = Mail(app)\n",
|
||
"The two environment variables that hold the email server username and password\n",
|
||
"need to be defined in the environment. If you are on Linux or macOS, you can set\n",
|
||
"these variables as follows:\n",
|
||
"(venv) $ export MAIL_USERNAME=<Gmail username>\n",
|
||
"(venv) $ export MAIL_PASSWORD=<Gmail password>\n",
|
||
"80 \n",
|
||
"| \n",
|
||
"Chapter 6: Email\n",
|
||
"\n",
|
||
"For Microsoft Windows users, the environment variables are set as follows:\n",
|
||
"(venv) $ set MAIL_USERNAME=<Gmail username>\n",
|
||
"(venv) $ set MAIL_PASSWORD=<Gmail password>\n",
|
||
"Sending Email from the Python Shell\n",
|
||
"To test the configuration, you can start a shell session and send a test email (replace\n",
|
||
"you@example.com with your own email address):\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> from flask_mail import Message\n",
|
||
">>> from hello import mail\n",
|
||
">>> msg = Message('test email', sender='you@example.com',\n",
|
||
"... recipients=['you@example.com'])\n",
|
||
">>> msg.body = 'This is the plain text body'\n",
|
||
">>> msg.html = 'This is the <b>HTML</b> body'\n",
|
||
">>> with app.app_context():\n",
|
||
"... mail.send(msg)\n",
|
||
"...\n",
|
||
"Note that Flask-Mail’s send() function uses current_app, so it needs to be executed\n",
|
||
"with an activated application context.\n",
|
||
"Integrating Emails with the Application\n",
|
||
"To avoid having to create email messages manually every time, it is a good idea to\n",
|
||
"abstract the common parts of the application’s email sending functionality into a\n",
|
||
"function. As an added benefit, this function can render email bodies from Jinja2 tem‐\n",
|
||
"plates to have the most flexibility. The implementation is shown in Example 6-3.\n",
|
||
"Example 6-3. hello.py: email support\n",
|
||
"from flask_mail import Message\n",
|
||
"app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'\n",
|
||
"app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'\n",
|
||
"def send_email(to, subject, template, **kwargs):\n",
|
||
" msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,\n",
|
||
" sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])\n",
|
||
" msg.body = render_template(template + '.txt', **kwargs)\n",
|
||
" msg.html = render_template(template + '.html', **kwargs)\n",
|
||
" mail.send(msg)\n",
|
||
"The function relies on two application-specific configuration keys that define a prefix\n",
|
||
"string for the subject and the address that will be used as the sender. The\n",
|
||
"send_email() function takes the destination address, a subject line, a template for the\n",
|
||
"email body, and a list of keyword arguments. The template name must be given\n",
|
||
"Email Support with Flask-Mail \n",
|
||
"| \n",
|
||
"81\n",
|
||
"\n",
|
||
"without the extension, so that two versions of the template can be used for the plain\n",
|
||
"text and HTML bodies. The keyword arguments passed by the caller are given to\n",
|
||
"render_template() so that they can be used by the templates that generate the email\n",
|
||
"body as template variables.\n",
|
||
"The index() view function can easily be expanded to send an email to the adminis‐\n",
|
||
"trator whenever a new name is received with the form. Example 6-4 shows this\n",
|
||
"change.\n",
|
||
"Example 6-4. hello.py: email example\n",
|
||
"# ...\n",
|
||
"app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')\n",
|
||
"# ...\n",
|
||
"@app.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" form = NameForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" user = User.query.filter_by(username=form.name.data).first()\n",
|
||
" if user is None:\n",
|
||
" user = User(username=form.name.data)\n",
|
||
" db.session.add(user)\n",
|
||
" session['known'] = False\n",
|
||
" if app.config['FLASKY_ADMIN']:\n",
|
||
" send_email(app.config['FLASKY_ADMIN'], 'New User',\n",
|
||
" 'mail/new_user', user=user)\n",
|
||
" else:\n",
|
||
" session['known'] = True\n",
|
||
" session['name'] = form.name.data\n",
|
||
" form.name.data = ''\n",
|
||
" return redirect(url_for('index'))\n",
|
||
" return render_template('index.html', form=form, name=session.get('name'),\n",
|
||
" known=session.get('known', False))\n",
|
||
"The recipient of the email is given in the FLASKY_ADMIN environment variable that’s\n",
|
||
"loaded into a configuration variable of the same name during startup. Two template\n",
|
||
"files need to be created for the text and HTML versions of the email. These files are\n",
|
||
"stored in a mail subdirectory inside templates to keep them separate from regular\n",
|
||
"templates. The email templates expect the user to be given as a template argument, so\n",
|
||
"the call to send_email() includes it as a keyword argument.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 6a to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"82 \n",
|
||
"| \n",
|
||
"Chapter 6: Email\n",
|
||
"\n",
|
||
"In addition to the MAIL_USERNAME and MAIL_PASSWORD environment variables\n",
|
||
"described earlier, this version of the application needs the FLASKY_ADMIN environment\n",
|
||
"variable. For Linux and macOS users, the command to set this variable is:\n",
|
||
"(venv) $ export FLASKY_ADMIN=<your-email-address>\n",
|
||
"For Microsoft Windows users, this is the equivalent command:\n",
|
||
"(venv) $ set FLASKY_ADMIN=<your-email-address>\n",
|
||
"With these environment variables set, you can test the application and receive an\n",
|
||
"email every time you enter a new name in the form.\n",
|
||
"Sending Asynchronous Email\n",
|
||
"If you sent a few test emails, you likely noticed that the mail.send() function blocks\n",
|
||
"for a few seconds while the email is sent, making the browser look unresponsive dur‐\n",
|
||
"ing that time. To avoid unnecessary delays during request handling, the email send\n",
|
||
"function can be moved to a background thread. Example 6-5 shows this change.\n",
|
||
"Example 6-5. hello.py: asynchronous email support\n",
|
||
"from threading import Thread\n",
|
||
"def send_async_email(app, msg):\n",
|
||
" with app.app_context():\n",
|
||
" mail.send(msg)\n",
|
||
"def send_email(to, subject, template, **kwargs):\n",
|
||
" msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,\n",
|
||
" sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])\n",
|
||
" msg.body = render_template(template + '.txt', **kwargs)\n",
|
||
" msg.html = render_template(template + '.html', **kwargs)\n",
|
||
" thr = Thread(target=send_async_email, args=[app, msg])\n",
|
||
" thr.start()\n",
|
||
" return thr\n",
|
||
"This implementation highlights an interesting problem. Many Flask extensions oper‐\n",
|
||
"ate under the assumption that there are active application and/or request contexts. As\n",
|
||
"mentioned previously, Flask-Mail’s send() function uses current_app, so it requires\n",
|
||
"the application context to be active. But since contexts are associated with a thread,\n",
|
||
"when the mail.send() function executes in a different thread it needs the application\n",
|
||
"context to be created artificially using app.app_context(). The app instance is passed\n",
|
||
"to the thread as an argument so that a context can be created.\n",
|
||
"Email Support with Flask-Mail \n",
|
||
"| \n",
|
||
"83\n",
|
||
"\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 6b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"If you run the application now, you will notice that it is much more responsive, but\n",
|
||
"keep in mind that for applications that send a large volume of email, having a job\n",
|
||
"dedicated to sending email is more appropriate than starting a new thread for each\n",
|
||
"email send operation. For example, the execution of the send_async_email() func‐\n",
|
||
"tion can be sent to a Celery task queue.\n",
|
||
"This chapter completes the overview of the features that are a must-have for most\n",
|
||
"web applications. The problem now is that the hello.py script is starting to get large,\n",
|
||
"and that makes it harder to work with. In the next chapter, you will learn how to\n",
|
||
"structure a larger application.\n",
|
||
"84 \n",
|
||
"| \n",
|
||
"Chapter 6: Email\n",
|
||
"\n",
|
||
"CHAPTER 7\n",
|
||
"Large Application Structure\n",
|
||
"Although having small web applications stored in a single script file can be very con‐\n",
|
||
"venient, this approach does not scale well. As the application grows in complexity,\n",
|
||
"working with a single large source file becomes problematic.\n",
|
||
"Unlike most other web frameworks, Flask does not impose a specific organization for\n",
|
||
"large projects; the way to structure the application is left entirely to the developer. In\n",
|
||
"this chapter, a possible way to organize a large application in packages and modules is\n",
|
||
"presented. This structure will be used in the remaining examples of the book.\n",
|
||
"Project Structure\n",
|
||
"Example 7-1 shows the basic layout for a Flask application.\n",
|
||
"Example 7-1. Basic multiple-file Flask application structure\n",
|
||
"|-flasky\n",
|
||
" |-app/\n",
|
||
" |-templates/\n",
|
||
" |-static/\n",
|
||
" |-main/\n",
|
||
" |-__init__.py\n",
|
||
" |-errors.py\n",
|
||
" |-forms.py\n",
|
||
" |-views.py\n",
|
||
" |-__init__.py\n",
|
||
" |-email.py\n",
|
||
" |-models.py\n",
|
||
" |-migrations/\n",
|
||
" |-tests/\n",
|
||
" |-__init__.py\n",
|
||
" |-test*.py\n",
|
||
" |-venv/\n",
|
||
" |-requirements.txt\n",
|
||
" |-config.py\n",
|
||
" |-flasky.py\n",
|
||
"85\n",
|
||
"\n",
|
||
"This structure has four top-level folders:\n",
|
||
"• The Flask application lives inside a package generically named app.\n",
|
||
"• The migrations folder contains the database migration scripts, as before.\n",
|
||
"• Unit tests are written in a tests package.\n",
|
||
"• The venv folder contains the Python virtual environment, as before.\n",
|
||
"There are also a few new files:\n",
|
||
"• requirements.txt lists the package dependencies so that it is easy to regenerate an\n",
|
||
"identical virtual environment on a different computer.\n",
|
||
"• config.py stores the configuration settings.\n",
|
||
"• flasky.py defines the Flask application instance, and also includes a few tasks that\n",
|
||
"help manage the application.\n",
|
||
"To help you fully understand this structure, the following sections describe the pro‐\n",
|
||
"cess to convert the hello.py application to it.\n",
|
||
"Configuration Options\n",
|
||
"Applications often need several configuration sets. The best example of this is the\n",
|
||
"need to use different databases during development, testing, and production so that\n",
|
||
"they don’t interfere with each other.\n",
|
||
"Instead of the simple app.config dictionary-like configuration used by hello.py, a\n",
|
||
"hierarchy of configuration classes can be used. Example 7-2 shows the config.py file,\n",
|
||
"with all the settings imported from hello.py.\n",
|
||
"Example 7-2. config.py: application configuration\n",
|
||
"import os\n",
|
||
"basedir = os.path.abspath(os.path.dirname(__file__))\n",
|
||
"class Config:\n",
|
||
" SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'\n",
|
||
" MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')\n",
|
||
" MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))\n",
|
||
" MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \\\n",
|
||
" ['true', 'on', '1']\n",
|
||
" MAIL_USERNAME = os.environ.get('MAIL_USERNAME')\n",
|
||
" MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')\n",
|
||
" FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'\n",
|
||
" FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'\n",
|
||
" FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')\n",
|
||
" SQLALCHEMY_TRACK_MODIFICATIONS = False\n",
|
||
"86 \n",
|
||
"| \n",
|
||
"Chapter 7: Large Application Structure\n",
|
||
"\n",
|
||
" @staticmethod\n",
|
||
" def init_app(app):\n",
|
||
" pass\n",
|
||
"class DevelopmentConfig(Config):\n",
|
||
" DEBUG = True\n",
|
||
" SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \\\n",
|
||
" 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')\n",
|
||
"class TestingConfig(Config):\n",
|
||
" TESTING = True\n",
|
||
" SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \\\n",
|
||
" 'sqlite://'\n",
|
||
"class ProductionConfig(Config):\n",
|
||
" SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \\\n",
|
||
" 'sqlite:///' + os.path.join(basedir, 'data.sqlite')\n",
|
||
"config = {\n",
|
||
" 'development': DevelopmentConfig,\n",
|
||
" 'testing': TestingConfig,\n",
|
||
" 'production': ProductionConfig,\n",
|
||
" 'default': DevelopmentConfig\n",
|
||
"}\n",
|
||
"The Config base class contains settings that are common to all configurations; the\n",
|
||
"different subclasses define settings that are specific to a configuration. Additional\n",
|
||
"configurations can be added as needed.\n",
|
||
"To make configuration more flexible and safe, most settings can be optionally impor‐\n",
|
||
"ted from environment variables. For example, the value of the SECRET_KEY, due to its\n",
|
||
"sensitive nature, can be set in the environment, but a default value is provided in case\n",
|
||
"the environment does not define it. Typically, these settings can be used with their\n",
|
||
"defaults during development but should each have an appropriate value set in the\n",
|
||
"corresponding environment variable on the production server. The configuration\n",
|
||
"options for the email server are all imported from environment variables as well, with\n",
|
||
"defaults pointing to the Gmail server for convenience during development.\n",
|
||
"Never write passwords or other secrets in a configuration file that is\n",
|
||
"committed to source control.\n",
|
||
"The SQLALCHEMY_DATABASE_URI variable is assigned different values under each of the\n",
|
||
"three configurations. This enables the application to use a different database in each\n",
|
||
"Configuration Options \n",
|
||
"| \n",
|
||
"87\n",
|
||
"\n",
|
||
"configuration. This is very important, as you don’t want a run of the unit tests to\n",
|
||
"change the database that you use for day-to-day development. Each configuration\n",
|
||
"tries to import the database URL from an environment variable, and when that is not\n",
|
||
"available it sets a default one based on SQLite. For the testing configuration, the\n",
|
||
"default is an in-memory database, since there is no need to store any data outside of\n",
|
||
"the test run.\n",
|
||
"The development and production configurations each have a set of mail server con‐\n",
|
||
"figuration options. As an additional way to allow the application to customize its con‐\n",
|
||
"figuration, the Config class and its subclasses can define an init_app() class method\n",
|
||
"that takes the application instance as an argument. For now the base Config class\n",
|
||
"implements an empty init_app() method.\n",
|
||
"At the bottom of the configuration script, the different configurations are registered\n",
|
||
"in a config dictionary. One of the configurations (the one for development, in this\n",
|
||
"case) is also registered as the default.\n",
|
||
"Application Package\n",
|
||
"The application package is where all the application code, templates, and static files\n",
|
||
"live. It is called simply app here, though it can be given an application-specific name if\n",
|
||
"desired. The templates and static directories are now part of the application package,\n",
|
||
"so they are moved inside app. The database models and the email support functions\n",
|
||
"are also moved inside this package, each in its own module, as app/models.py and\n",
|
||
"app/email.py.\n",
|
||
"Using an Application Factory\n",
|
||
"The way the application is created in the single-file version is very convenient, but it\n",
|
||
"has one big drawback. Because the application is created in the global scope, there is\n",
|
||
"no way to apply configuration changes dynamically: by the time the script is running,\n",
|
||
"the application instance has already been created, so it is already too late to make\n",
|
||
"configuration changes. This is particularly important for unit tests because sometimes\n",
|
||
"it is necessary to run the application under different configuration settings for better\n",
|
||
"test coverage.\n",
|
||
"The solution to this problem is to delay the creation of the application by moving it\n",
|
||
"into a factory function that can be explicitly invoked from the script. This not only\n",
|
||
"gives the script time to set the configuration, but also the ability to create multiple\n",
|
||
"application instances—another thing that can be very useful during testing. The\n",
|
||
"application factory function, shown in Example 7-3, is defined in the app package\n",
|
||
"constructor.\n",
|
||
"88 \n",
|
||
"| \n",
|
||
"Chapter 7: Large Application Structure\n",
|
||
"\n",
|
||
"Example 7-3. app/__init__.py: application package constructor\n",
|
||
"from flask import Flask, render_template\n",
|
||
"from flask_bootstrap import Bootstrap\n",
|
||
"from flask_mail import Mail\n",
|
||
"from flask_moment import Moment\n",
|
||
"from flask_sqlalchemy import SQLAlchemy\n",
|
||
"from config import config\n",
|
||
"bootstrap = Bootstrap()\n",
|
||
"mail = Mail()\n",
|
||
"moment = Moment()\n",
|
||
"db = SQLAlchemy()\n",
|
||
"def create_app(config_name):\n",
|
||
" app = Flask(__name__)\n",
|
||
" app.config.from_object(config[config_name])\n",
|
||
" config[config_name].init_app(app)\n",
|
||
" bootstrap.init_app(app)\n",
|
||
" mail.init_app(app)\n",
|
||
" moment.init_app(app)\n",
|
||
" db.init_app(app)\n",
|
||
" # attach routes and custom error pages here\n",
|
||
" return app\n",
|
||
"This constructor imports most of the Flask extensions currently in use, but because\n",
|
||
"there is no application instance to initialize them with, it creates them uninitialized\n",
|
||
"by passing no arguments into their constructors. The create_app() function is the\n",
|
||
"application factory, which takes as an argument the name of a configuration to use\n",
|
||
"for the application. The configuration settings stored in one of the classes defined in\n",
|
||
"config.py can be imported directly into the application using the from_object()\n",
|
||
"method available in Flask’s app.config configuration object. The configuration\n",
|
||
"object is selected by name from the config dictionary. Once an application is created\n",
|
||
"and configured, the extensions can be initialized. Calling init_app() on the exten‐\n",
|
||
"sions that were created earlier completes their initialization.\n",
|
||
"The application initialization is now done in this factory function, using the\n",
|
||
"from_object() method from the Flask configuration object, which takes as an argu‐\n",
|
||
"ment one of the configuration classes defined in config.py. The init_app() method of\n",
|
||
"the selected configuration is also invoked, to allow more complex initialization proce‐\n",
|
||
"dures to take place.\n",
|
||
"The factory function returns the created application instance, but note that applica‐\n",
|
||
"tions created with the factory function in its current state are incomplete, as they are\n",
|
||
"missing routes and custom error page handlers. This is the topic of the next section.\n",
|
||
"Application Package \n",
|
||
"| \n",
|
||
"89\n",
|
||
"\n",
|
||
"Implementing Application Functionality in a Blueprint\n",
|
||
"The conversion to an application factory introduces a complication for routes. In\n",
|
||
"single-script applications, the application instance exists in the global scope, so routes\n",
|
||
"can be easily defined using the app.route decorator. But now that the application is\n",
|
||
"created at runtime, the app.route decorator begins to exist only after create_app()\n",
|
||
"is invoked, which is too late. Custom error page handlers present the same problem,\n",
|
||
"as these are defined with the app.errorhandler decorator.\n",
|
||
"Luckily, Flask offers a better solution using blueprints. A blueprint is similar to an\n",
|
||
"application in that it can also define routes and error handlers. The difference is that\n",
|
||
"when these are defined in a blueprint they are in a dormant state until the blueprint is\n",
|
||
"registered with an application, at which point they become part of it. Using a blue‐\n",
|
||
"print defined in the global scope, the routes and error handlers of the application can\n",
|
||
"be defined in almost the same way as in the single-script application.\n",
|
||
"Like applications, blueprints can be defined all in a single file or can be created in a\n",
|
||
"more structured way with multiple modules inside a package. To allow for the great‐\n",
|
||
"est flexibility, a subpackage inside the application package will be created to host the\n",
|
||
"first blueprint of the application. Example 7-4 shows the package constructor, which\n",
|
||
"creates the blueprint.\n",
|
||
"Example 7-4. app/main/__init__.py: main blueprint creation\n",
|
||
"from flask import Blueprint\n",
|
||
"main = Blueprint('main', __name__)\n",
|
||
"from . import views, errors\n",
|
||
"Blueprints are created by instantiating an object of class Blueprint. The constructor\n",
|
||
"for this class takes two required arguments: the blueprint name and the module or\n",
|
||
"package where the blueprint is located. As with applications, Python’s __name__ vari‐\n",
|
||
"able is in most cases the correct value for the second argument.\n",
|
||
"The routes of the application are stored in an app/main/views.py module inside the\n",
|
||
"package, and the error handlers are in app/main/errors.py. Importing these modules\n",
|
||
"causes the routes and error handlers to be associated with the blueprint. It is impor‐\n",
|
||
"tant to note that the modules are imported at the bottom of the app/main/__init__.py\n",
|
||
"script to avoid errors due to circular dependencies. In this particular example the\n",
|
||
"problem is that app/main/views.py and app/main/errors.py in turn are going to import\n",
|
||
"the main blueprint object, so the imports are going to fail unless the circular reference\n",
|
||
"occurs after main is defined.\n",
|
||
"90 \n",
|
||
"| \n",
|
||
"Chapter 7: Large Application Structure\n",
|
||
"\n",
|
||
"The from . import <some-module> syntax is used in Python to\n",
|
||
"represent relative imports. The . in this statement represents the\n",
|
||
"current package. You are going to see another very useful relative\n",
|
||
"import soon that uses the form from .. import <some-module>,\n",
|
||
"where .. represents the parent of the current package.\n",
|
||
"The blueprint is registered with the application inside the create_app() factory func‐\n",
|
||
"tion, as shown in Example 7-5.\n",
|
||
"Example 7-5. app/__init__.py: main blueprint registration\n",
|
||
"def create_app(config_name):\n",
|
||
" # ...\n",
|
||
" from .main import main as main_blueprint\n",
|
||
" app.register_blueprint(main_blueprint)\n",
|
||
" return app\n",
|
||
"Example 7-6 shows the error handlers.\n",
|
||
"Example 7-6. app/main/errors.py: error handlers in main blueprint\n",
|
||
"from flask import render_template\n",
|
||
"from . import main\n",
|
||
"@main.app_errorhandler(404)\n",
|
||
"def page_not_found(e):\n",
|
||
" return render_template('404.html'), 404\n",
|
||
"@main.app_errorhandler(500)\n",
|
||
"def internal_server_error(e):\n",
|
||
" return render_template('500.html'), 500\n",
|
||
"A difference when writing error handlers inside a blueprint is that if the\n",
|
||
"errorhandler decorator is used, the handler will be invoked only for errors that orig‐\n",
|
||
"inate in the routes defined by the blueprint. To install application-wide error han‐\n",
|
||
"dlers, the app_errorhandler decorator must be used instead.\n",
|
||
"Example 7-7 shows the route of the application updated to be in the blueprint.\n",
|
||
"Application Package \n",
|
||
"| \n",
|
||
"91\n",
|
||
"\n",
|
||
"Example 7-7. app/main/views.py: application routes in main blueprint\n",
|
||
"from datetime import datetime\n",
|
||
"from flask import render_template, session, redirect, url_for\n",
|
||
"from . import main\n",
|
||
"from .forms import NameForm\n",
|
||
"from .. import db\n",
|
||
"from ..models import User\n",
|
||
"@main.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" form = NameForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" # ...\n",
|
||
" return redirect(url_for('.index'))\n",
|
||
" return render_template('index.html',\n",
|
||
" form=form, name=session.get('name'),\n",
|
||
" known=session.get('known', False),\n",
|
||
" current_time=datetime.utcnow())\n",
|
||
"There are two main differences when writing a view function inside a blueprint. First,\n",
|
||
"as was done for error handlers earlier, the route decorator comes from the blueprint,\n",
|
||
"so main.route is used instead of app.route. The second difference is in the usage of\n",
|
||
"the url_for() function. As you may recall, the first argument to this function is the\n",
|
||
"endpoint name of the route, which for application-based routes defaults to the name\n",
|
||
"of the view function. For example, in a single-script application the URL for an\n",
|
||
"index() view function can be obtained with url_for('index').\n",
|
||
"The difference with blueprints is that Flask applies a namespace to all the endpoints\n",
|
||
"defined in a blueprint, so that multiple blueprints can define view functions with the\n",
|
||
"same endpoint names without collisions. The namespace is the name of the blueprint\n",
|
||
"(the first argument to the Blueprint constructor) and is separated from the endpoint\n",
|
||
"name with a dot. The index() view function is then registered with endpoint name\n",
|
||
"main.index and its URL can be obtained with url_for('main.index').\n",
|
||
"The url_for() function also supports a shorter format for endpoints in blueprints in\n",
|
||
"which the blueprint name is omitted, such as url_for('.index'). With this nota‐\n",
|
||
"tion, the blueprint name for the current request is used to complete the endpoint\n",
|
||
"name. This effectively means that redirects within the same blueprint can use the\n",
|
||
"shorter form, while redirects across blueprints must use the fully qualified endpoint\n",
|
||
"name that includes the blueprint name.\n",
|
||
"To complete the changes to the application package, the form objects are also stored\n",
|
||
"inside the blueprint in the app/main/forms.py module.\n",
|
||
"92 \n",
|
||
"| \n",
|
||
"Chapter 7: Large Application Structure\n",
|
||
"\n",
|
||
"Application Script\n",
|
||
"The flasky.py module in the top-level directory is where the application instance is\n",
|
||
"defined. This script is shown in Example 7-8.\n",
|
||
"Example 7-8. flasky.py: main script\n",
|
||
"import os\n",
|
||
"from app import create_app, db\n",
|
||
"from app.models import User, Role\n",
|
||
"from flask_migrate import Migrate\n",
|
||
"app = create_app(os.getenv('FLASK_CONFIG') or 'default')\n",
|
||
"migrate = Migrate(app, db)\n",
|
||
"@app.shell_context_processor\n",
|
||
"def make_shell_context():\n",
|
||
" return dict(db=db, User=User, Role=Role)\n",
|
||
"The script begins by creating an application. The configuration is taken from the\n",
|
||
"environment variable FLASK_CONFIG if it’s defined, or else the default configuration is\n",
|
||
"used. Flask-Migrate and the custom context for the Python shell are then initialized.\n",
|
||
"Because the main script of the application changed from hello.py to flasky.py, the\n",
|
||
"FLASK_APP environment variable needs to be updated accordingly so that the flask\n",
|
||
"command can locate the application instance. It is also useful to enable Flask’s debug\n",
|
||
"mode by setting FLASK_DEBUG=1. For Linux and macOS, this is all done as follows:\n",
|
||
"(venv) $ export FLASK_APP=flasky.py\n",
|
||
"(venv) $ export FLASK_DEBUG=1\n",
|
||
"And for Microsoft Windows:\n",
|
||
"(venv) $ set FLASK_APP=flasky.py\n",
|
||
"(venv) $ set FLASK_DEBUG=1\n",
|
||
"Requirements File\n",
|
||
"It is a good practice for applications to include a requirements.txt file that records all\n",
|
||
"the package dependencies, with the exact version numbers. This is important in case\n",
|
||
"the virtual environment needs to be regenerated on a different machine, such as the\n",
|
||
"machine on which the application will be deployed for production use. This file can\n",
|
||
"be generated automatically by pip with the following command:\n",
|
||
"(venv) $ pip freeze >requirements.txt\n",
|
||
"It is a good idea to refresh this file whenever a package is installed or upgraded. An\n",
|
||
"example requirements file is shown here:\n",
|
||
"Application Script \n",
|
||
"| \n",
|
||
"93\n",
|
||
"\n",
|
||
"alembic==0.9.3\n",
|
||
"blinker==1.4\n",
|
||
"click==6.7\n",
|
||
"dominate==2.3.1\n",
|
||
"Flask==0.12.2\n",
|
||
"Flask-Bootstrap==3.3.7.1\n",
|
||
"Flask-Mail==0.9.1\n",
|
||
"Flask-Migrate==2.0.4\n",
|
||
"Flask-Moment==0.5.1\n",
|
||
"Flask-SQLAlchemy==2.2\n",
|
||
"Flask-WTF==0.14.2\n",
|
||
"itsdangerous==0.24\n",
|
||
"Jinja2==2.9.6\n",
|
||
"Mako==1.0.7\n",
|
||
"MarkupSafe==1.0\n",
|
||
"python-dateutil==2.6.1\n",
|
||
"python-editor==1.0.3\n",
|
||
"six==1.10.0\n",
|
||
"SQLAlchemy==1.1.11\n",
|
||
"visitor==0.1.3\n",
|
||
"Werkzeug==0.12.2\n",
|
||
"WTForms==2.1\n",
|
||
"When you need to build a perfect replica of the virtual environment, you can create a\n",
|
||
"new virtual environment and run the following command on it:\n",
|
||
"(venv) $ pip install -r requirements.txt\n",
|
||
"The version numbers in the example requirements.txt file are likely going to be outda‐\n",
|
||
"ted by the time you read this. You can try using more recent releases of the packages,\n",
|
||
"if you like. If you experience any problems, you can always go back to the versions\n",
|
||
"specified here, as those are known to be compatible with the application.\n",
|
||
"Unit Tests\n",
|
||
"This application is very small, so there isn’t a lot to test yet. But as an example, two\n",
|
||
"simple tests can be defined, as shown in Example 7-9.\n",
|
||
"Example 7-9. tests/test_basics.py: unit tests\n",
|
||
"import unittest\n",
|
||
"from flask import current_app\n",
|
||
"from app import create_app, db\n",
|
||
"class BasicsTestCase(unittest.TestCase):\n",
|
||
" def setUp(self):\n",
|
||
" self.app = create_app('testing')\n",
|
||
" self.app_context = self.app.app_context()\n",
|
||
" self.app_context.push()\n",
|
||
" db.create_all()\n",
|
||
"94 \n",
|
||
"| \n",
|
||
"Chapter 7: Large Application Structure\n",
|
||
"\n",
|
||
" def tearDown(self):\n",
|
||
" db.session.remove()\n",
|
||
" db.drop_all()\n",
|
||
" self.app_context.pop()\n",
|
||
" def test_app_exists(self):\n",
|
||
" self.assertFalse(current_app is None)\n",
|
||
" def test_app_is_testing(self):\n",
|
||
" self.assertTrue(current_app.config['TESTING'])\n",
|
||
"The tests are written using the standard unittest package from the Python standard\n",
|
||
"library. The setUp() and tearDown() methods of the test case class run before and\n",
|
||
"after each test, and any methods that have a name that begins with test_ are executed\n",
|
||
"as tests.\n",
|
||
"If you want to learn more about writing unit tests with Python’s\n",
|
||
"unittest package, read the official documentation.\n",
|
||
"The setUp() method tries to create an environment for the test that is close to that of\n",
|
||
"a running application. It first creates an application configured for testing and acti‐\n",
|
||
"vates its context. This step ensures that tests have access to current_app, like regular\n",
|
||
"requests do. Then it creates a brand-new database for the tests using Flask-\n",
|
||
"SQLAlchemy’s create_all() method. The database and the application context are\n",
|
||
"removed in the tearDown() method.\n",
|
||
"The first test ensures that the application instance exists. The second test ensures that\n",
|
||
"the application is running under the testing configuration. To make the tests directory\n",
|
||
"a proper package, a tests/init.py module needs to be added, but this can be an empty\n",
|
||
"file, as the unittest package scans all the modules to discover the tests.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 7a to check out the converted version of the\n",
|
||
"application. To ensure that you have all the dependencies installed,\n",
|
||
"also run pip install -r requirements.txt.\n",
|
||
"To run the unit tests, a custom command can be added to the flasky.py script.\n",
|
||
"Example 7-10 shows how to add a test command.\n",
|
||
"Unit Tests \n",
|
||
"| \n",
|
||
"95\n",
|
||
"\n",
|
||
"Example 7-10. flasky.py: unit test launcher command\n",
|
||
"@app.cli.command()\n",
|
||
"def test():\n",
|
||
" \"\"\"Run the unit tests.\"\"\"\n",
|
||
" import unittest\n",
|
||
" tests = unittest.TestLoader().discover('tests')\n",
|
||
" unittest.TextTestRunner(verbosity=2).run(tests)\n",
|
||
"The app.cli.command decorator makes it simple to implement custom commands.\n",
|
||
"The name of the decorated function is used as the command name, and the function’s\n",
|
||
"docstring is displayed in the help messages. The implementation of the test() func‐\n",
|
||
"tion invokes the test runner from the unittest package.\n",
|
||
"The unit tests can be executed as follows:\n",
|
||
"(venv) $ flask test\n",
|
||
"test_app_exists (test_basics.BasicsTestCase) ... ok\n",
|
||
"test_app_is_testing (test_basics.BasicsTestCase) ... ok\n",
|
||
".----------------------------------------------------------------------\n",
|
||
"Ran 2 tests in 0.001s\n",
|
||
"OK\n",
|
||
"----\n",
|
||
"Database Setup\n",
|
||
"The restructured application uses a different database than the single-script version.\n",
|
||
"The database URL is taken from an environment variable as a first choice, with a\n",
|
||
"default SQLite database as an alternative. The environment variables and SQLite\n",
|
||
"database filenames are different for each of the three configurations. For example, in\n",
|
||
"the development configuration the URL is obtained from the environment variable\n",
|
||
"DEV_DATABASE_URL, and if that is not defined then an SQLite database with the name\n",
|
||
"data-dev.sqlite is used.\n",
|
||
"Regardless of the source of the database URL, the database tables must be created for\n",
|
||
"the new database. When working with Flask-Migrate to keep track of migrations,\n",
|
||
"database tables can be created or upgraded to the latest revision with a single com‐\n",
|
||
"mand:\n",
|
||
"(venv) $ flask db upgrade\n",
|
||
"96 \n",
|
||
"| \n",
|
||
"Chapter 7: Large Application Structure\n",
|
||
"\n",
|
||
"Running the Application\n",
|
||
"The refactoring is now complete, and the application can be started. Make sure you\n",
|
||
"have updated the FLASK_APP environment variable as indicated in “Application Script”\n",
|
||
"on page 93, and then run the application as usual:\n",
|
||
"(venv) $ flask run\n",
|
||
"Having to set the FLASK_APP and FLASK_DEBUG environment variables every time a\n",
|
||
"new command-prompt session is started can get tedious, so you should configure\n",
|
||
"your system so that these variables are set by default. If you are using bash, you can\n",
|
||
"add them to your ~/.bashrc file.\n",
|
||
"Believe it or not, you have reached the end of Part I. You have now learned about the\n",
|
||
"basic elements necessary to build a web application with Flask, but you probably feel\n",
|
||
"unsure about how all these pieces fit together to form a real application. The goal of\n",
|
||
"Part II is to help with that by walking you through the development of a complete\n",
|
||
"application.\n",
|
||
"Running the Application \n",
|
||
"| \n",
|
||
"97\n",
|
||
"\n",
|
||
"\n",
|
||
"PART II\n",
|
||
"Example: A Social\n",
|
||
"Blogging Application\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 8\n",
|
||
"User Authentication\n",
|
||
"Most applications need to keep track of who their users are. When users connect with\n",
|
||
"an application, they authenticate with it, a process by which they make their identity\n",
|
||
"known. Once the application knows who the user is, it can offer a customized experi‐\n",
|
||
"ence.\n",
|
||
"The most commonly used method of authentication requires users to provide a piece\n",
|
||
"of identification, which is either their email address or username, and a secret only\n",
|
||
"known to them, which is called the password. In this chapter, the complete authenti‐\n",
|
||
"cation system for Flasky is created.\n",
|
||
"Authentication Extensions for Flask\n",
|
||
"There are many excellent Python authentication packages, but none of them do\n",
|
||
"everything. The user authentication solution presented in this chapter uses several\n",
|
||
"packages and provides the glue that makes them work well together. This is the list of\n",
|
||
"packages that will be used, and what they’re used for:\n",
|
||
"• Flask-Login: Management of user sessions for logged-in users\n",
|
||
"• Werkzeug: Password hashing and verification\n",
|
||
"• itsdangerous: Cryptographically secure token generation and verification\n",
|
||
"In addition to authentication-specific packages, the following general-purpose exten‐\n",
|
||
"sions will be used:\n",
|
||
"• Flask-Mail: Sending of authentication-related emails\n",
|
||
"• Flask-Bootstrap: HTML templates\n",
|
||
"• Flask-WTF: Web forms\n",
|
||
"101\n",
|
||
"\n",
|
||
"Password Security\n",
|
||
"The safety of user information stored in databases is often overlooked during the\n",
|
||
"design of web applications. If an attacker is able to break into your server and access\n",
|
||
"your user database, then you risk the security of your users—and the risk is bigger\n",
|
||
"than you think. It is a known fact that most users use the same password on multiple\n",
|
||
"sites, so even if you don’t store any sensitive information, access to the passwords\n",
|
||
"stored in your database can give the attacker access to accounts your users have on\n",
|
||
"other sites.\n",
|
||
"The key to storing user passwords securely in a database relies on not storing the\n",
|
||
"password itself but a hash of it. A password hashing function takes a password as\n",
|
||
"input, adds a random component to it (the salt), and then applies several one-way\n",
|
||
"cryptographic transformations to it. The result is a new sequence of characters that\n",
|
||
"has no resemblance to the original password, and has no known way to be trans‐\n",
|
||
"formed back into the original password. Password hashes can be verified in place of\n",
|
||
"the real passwords because hashing functions are repeatable: given the same inputs\n",
|
||
"(the password and the salt), the result is always the same.\n",
|
||
"Password hashing is a complex task that is hard to get right. It is\n",
|
||
"recommended that you don’t implement your own solution but\n",
|
||
"instead rely on well-known libraries that have been reviewed by the\n",
|
||
"community. In the next section, Werkzeug’s password hashing\n",
|
||
"functions will be demonstrated. Other good choices for password\n",
|
||
"hashing are bcrypt and Passlib. If you are interested in learning\n",
|
||
"what’s involved in generating secure password hashes, the article\n",
|
||
"“Salted Password Hashing - Doing It Right” by Defuse Security is a\n",
|
||
"worthwhile read.\n",
|
||
"Hashing Passwords with Werkzeug\n",
|
||
"Werkzeug’s security module conveniently implements secure password hashing. This\n",
|
||
"functionality is exposed with just two functions, used in the registration and verifica‐\n",
|
||
"tion phases, respectively:\n",
|
||
"generate_password_hash(password, method='pbkdf2:sha256', salt_length=8)\n",
|
||
"This function takes a plain-text password and returns the password hash as a\n",
|
||
"string that can be stored in the user database. The default values for method and\n",
|
||
"salt_length are sufficient for most use cases.\n",
|
||
"check_password_hash(hash, password)\n",
|
||
"This function takes a password hash previously stored in the database and the\n",
|
||
"password entered by the user. A return value of True indicates that the user pass‐\n",
|
||
"word is correct.\n",
|
||
"102 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"Example 8-1 shows the changes to the User model created in Chapter 5 to accommo‐\n",
|
||
"date password hashing.\n",
|
||
"Example 8-1. app/models.py: password hashing in the User model\n",
|
||
"from werkzeug.security import generate_password_hash, check_password_hash\n",
|
||
"class User(db.Model):\n",
|
||
" # ...\n",
|
||
" password_hash = db.Column(db.String(128))\n",
|
||
" @property\n",
|
||
" def password(self):\n",
|
||
" raise AttributeError('password is not a readable attribute')\n",
|
||
" @password.setter\n",
|
||
" def password(self, password):\n",
|
||
" self.password_hash = generate_password_hash(password)\n",
|
||
" def verify_password(self, password):\n",
|
||
" return check_password_hash(self.password_hash, password)\n",
|
||
"The password hashing function is implemented through a write-only property called\n",
|
||
"password. When this property is set, the setter method will call Werkzeug’s\n",
|
||
"generate_password_hash() function and write the result to the password_hash field.\n",
|
||
"Attempting to read the password property will return an error, as clearly the original\n",
|
||
"password cannot be recovered once hashed.\n",
|
||
"The verify_password() method takes a password and passes it to Werkzeug’s\n",
|
||
"check_password_hash() function for verification against the hashed version stored\n",
|
||
"in the User model. If this method returns True, then the password is correct.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 8a to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Password Security \n",
|
||
"| \n",
|
||
"103\n",
|
||
"\n",
|
||
"The password hashing functionality is now complete and can be tested in the shell:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> u = User()\n",
|
||
">>> u.password = 'cat'\n",
|
||
">>> u.password\n",
|
||
"Traceback (most recent call last):\n",
|
||
" File \"<console>\", line 1, in <module>\n",
|
||
" File \"/home/flask/flasky/app/models.py\", line 24, in password\n",
|
||
" raise AttributeError('password is not a readable attribute')\n",
|
||
"AttributeError: password is not a readable attribute\n",
|
||
">>> u.password_hash\n",
|
||
"'pbkdf2:sha256:50000$moHwFH1B$ef1574909f9c549285e8547cad181c5e0213cfa44a4aba4349\n",
|
||
"fa830aa1fd227f'\n",
|
||
">>> u.verify_password('cat')\n",
|
||
"True\n",
|
||
">>> u.verify_password('dog')\n",
|
||
"False\n",
|
||
">>> u2 = User()\n",
|
||
">>> u2.password = 'cat'\n",
|
||
">>> u2.password_hash\n",
|
||
"'pbkdf2:sha256:50000$Pfz0m0KU$27be930b7f0e0119d38e8d8a62f7f5e75c0a7db61ae16709bc\n",
|
||
"aa6cfd60c44b74'\n",
|
||
"Note how trying to access the password property of a user returns an\n",
|
||
"AttributeError. Also, users u and u2 have completely different password hashes,\n",
|
||
"even though they both use the same password. To ensure that this functionality con‐\n",
|
||
"tinues to work in the future, the preceding tests done manually can be written as unit\n",
|
||
"tests that can be repeated easily. In Example 8-2 a new module inside the tests package\n",
|
||
"is shown with three new tests that exercise the recent changes to the User model.\n",
|
||
"Example 8-2. tests/test_user_model.py: password hashing tests\n",
|
||
"import unittest\n",
|
||
"from app.models import User\n",
|
||
"class UserModelTestCase(unittest.TestCase):\n",
|
||
" def test_password_setter(self):\n",
|
||
" u = User(password = 'cat')\n",
|
||
" self.assertTrue(u.password_hash is not None)\n",
|
||
" def test_no_password_getter(self):\n",
|
||
" u = User(password = 'cat')\n",
|
||
" with self.assertRaises(AttributeError):\n",
|
||
" u.password\n",
|
||
" def test_password_verification(self):\n",
|
||
" u = User(password = 'cat')\n",
|
||
" self.assertTrue(u.verify_password('cat'))\n",
|
||
" self.assertFalse(u.verify_password('dog'))\n",
|
||
"104 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
" def test_password_salts_are_random(self):\n",
|
||
" u = User(password='cat')\n",
|
||
" u2 = User(password='cat')\n",
|
||
" self.assertTrue(u.password_hash != u2.password_hash)\n",
|
||
"To run these new unit tests, use the following command:\n",
|
||
"(venv) $ flask test\n",
|
||
"test_app_exists (test_basics.BasicsTestCase) ... ok\n",
|
||
"test_app_is_testing (test_basics.BasicsTestCase) ... ok\n",
|
||
"test_no_password_getter (test_user_model.UserModelTestCase) ... ok\n",
|
||
"test_password_salts_are_random (test_user_model.UserModelTestCase) ... ok\n",
|
||
"test_password_setter (test_user_model.UserModelTestCase) ... ok\n",
|
||
"test_password_verification (test_user_model.UserModelTestCase) ... ok\n",
|
||
".----------------------------------------------------------------------\n",
|
||
"Ran 6 tests in 0.379s\n",
|
||
"OK\n",
|
||
"You can run the unit test suite like this every time you want to confirm everything is\n",
|
||
"working as expected. Having the automation in place makes verifying this feature\n",
|
||
"very low cost, so testing should be repeated often, to ensure that this functionality\n",
|
||
"does not break in the future.\n",
|
||
"Creating an Authentication Blueprint\n",
|
||
"Blueprints were introduced in Chapter 7 as a way to define routes in the global scope\n",
|
||
"after the creation of the application was moved into a factory function. In this sec‐\n",
|
||
"tion, the routes related to the user authentication subsystem will be added to a second\n",
|
||
"blueprint, called auth. Using different blueprints for different subsystems of the\n",
|
||
"application is a great way to keep the code neatly organized.\n",
|
||
"The auth blueprint will be hosted in a Python package with the same name. The blue‐\n",
|
||
"print’s package constructor creates the blueprint object and imports routes from a\n",
|
||
"views.py module. This is shown in Example 8-3.\n",
|
||
"Example 8-3. app/auth/__init__.py: authentication blueprint creation\n",
|
||
"from flask import Blueprint\n",
|
||
"auth = Blueprint('auth', __name__)\n",
|
||
"from . import views\n",
|
||
"The app/auth/views.py module, shown in Example 8-4, imports the blueprint and\n",
|
||
"defines the routes associated with authentication using its route decorator. For now,\n",
|
||
"a /login route is added, which renders a placeholder template of the same name.\n",
|
||
"Creating an Authentication Blueprint \n",
|
||
"| \n",
|
||
"105\n",
|
||
"\n",
|
||
"Example 8-4. app/auth/views.py: authentication blueprint routes and view functions\n",
|
||
"from flask import render_template\n",
|
||
"from . import auth\n",
|
||
"@auth.route('/login')\n",
|
||
"def login():\n",
|
||
" return render_template('auth/login.html')\n",
|
||
"Note that the template file given to render_template() is stored inside the auth\n",
|
||
"directory. This directory must be created inside app/templates, as Flask expects the\n",
|
||
"templates’ paths to be relative to the application’s templates directory. By storing the\n",
|
||
"blueprint templates in their own subdirectory, there is no risk of naming collisions\n",
|
||
"with the main blueprint or any other blueprints that will be added in the future.\n",
|
||
"Blueprints can also be configured to have their own independent\n",
|
||
"directories for templates. When multiple template directories have\n",
|
||
"been configured, the render_template() function searches the\n",
|
||
"templates directory configured for the application first, and then\n",
|
||
"searches the template directories defined by blueprints.\n",
|
||
"The auth blueprint needs to be attached to the application in the create_app() fac‐\n",
|
||
"tory function, as shown in Example 8-5.\n",
|
||
"Example 8-5. app/__init__.py: authentication blueprint registration\n",
|
||
"def create_app(config_name):\n",
|
||
" # ...\n",
|
||
" from .auth import auth as auth_blueprint\n",
|
||
" app.register_blueprint(auth_blueprint, url_prefix='/auth')\n",
|
||
" return app\n",
|
||
"The url_prefix argument in the blueprint registration is optional. When used, all\n",
|
||
"the routes defined in the blueprint will be registered with the given prefix, in this\n",
|
||
"case /auth. For example, the /login route will be registered as /auth/login, and the fully\n",
|
||
"qualified URL under the development web server then becomes http://localhost:5000/\n",
|
||
"auth/login.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 8b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"106 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"User Authentication with Flask-Login\n",
|
||
"When users log in to the application, their authenticated state has to be recorded in\n",
|
||
"the user session, so that it is remembered as they navigate through different pages.\n",
|
||
"Flask-Login is a small but extremely useful extension that specializes in managing\n",
|
||
"this particular aspect of a user authentication system, without being tied to a specific\n",
|
||
"authentication mechanism.\n",
|
||
"To begin, the extension needs to be installed in the virtual environment:\n",
|
||
"(venv) $ pip install flask-login\n",
|
||
"Preparing the User Model for Logins\n",
|
||
"Flask-Login works closely with the application’s own User objects. To be able to work\n",
|
||
"with the application’s User model, the Flask-Login extension requires it to implement\n",
|
||
"a few common properties and methods. The required items are shown in Table 8-1.\n",
|
||
"Table 8-1. Flask-Login required items\n",
|
||
"Property/method\n",
|
||
"Description\n",
|
||
"is_authenticated Must be True if the user has valid login credentials or False otherwise.\n",
|
||
"is_active\n",
|
||
"Must be True if the user is allowed to log in or False otherwise. A False value can be used for\n",
|
||
"disabled accounts.\n",
|
||
"is_anonymous\n",
|
||
"Must always be False for regular users and True for a special user object that represents\n",
|
||
"anonymous users.\n",
|
||
"get_id()\n",
|
||
"Must return a unique identifier for the user, encoded as a Unicode string.\n",
|
||
"These properties and methods can be implemented directly in the model class, but as\n",
|
||
"an easier alternative Flask-Login provides a UserMixin class that has default imple‐\n",
|
||
"mentations that are appropriate for most cases. The updated User model is shown in\n",
|
||
"Example 8-6.\n",
|
||
"Example 8-6. app/models.py: updates to the User model to support user logins\n",
|
||
"from flask_login import UserMixin\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" __tablename__ = 'users'\n",
|
||
" id = db.Column(db.Integer, primary_key = True)\n",
|
||
" email = db.Column(db.String(64), unique=True, index=True)\n",
|
||
" username = db.Column(db.String(64), unique=True, index=True)\n",
|
||
" password_hash = db.Column(db.String(128))\n",
|
||
" role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))\n",
|
||
"User Authentication with Flask-Login \n",
|
||
"| \n",
|
||
"107\n",
|
||
"\n",
|
||
"Note that an email field was also added. In this application, users will log in with\n",
|
||
"their email addresses, as they are less likely to forget those than their usernames.\n",
|
||
"Flask-Login is initialized in the application factory function, as shown in\n",
|
||
"Example 8-7.\n",
|
||
"Example 8-7. app/__init__.py: Flask-Login initialization\n",
|
||
"from flask_login import LoginManager\n",
|
||
"login_manager = LoginManager()\n",
|
||
"login_manager.login_view = 'auth.login'\n",
|
||
"def create_app(config_name):\n",
|
||
" # ...\n",
|
||
" login_manager.init_app(app)\n",
|
||
" # ...\n",
|
||
"The login_view attribute of the LoginManager object sets the endpoint for the login\n",
|
||
"page. Flask-Login will redirect to the login page when an anonymous user tries to\n",
|
||
"access a protected page. Because the login route is inside a blueprint, it needs to be\n",
|
||
"prefixed with the blueprint name.\n",
|
||
"Finally, Flask-Login requires the application to designate a function to be invoked\n",
|
||
"when the extension needs to load a user from the database given its identifier. This\n",
|
||
"function is shown in Example 8-8.\n",
|
||
"Example 8-8. app/models.py: user loader function\n",
|
||
"from . import login_manager\n",
|
||
"@login_manager.user_loader\n",
|
||
"def load_user(user_id):\n",
|
||
" return User.query.get(int(user_id))\n",
|
||
"The login_manager.user_loader decorator is used to register the function with\n",
|
||
"Flask-Login, which will call it when it needs to retrieve information about the logged-\n",
|
||
"in user. The user identifier will be passed as a string, so the function converts it to an\n",
|
||
"integer before it passes it to the Flask-SQLAlchemy query that loads the user. The\n",
|
||
"return value of the function must be the user object, or None if the user identifier is\n",
|
||
"invalid or any other error occurred.\n",
|
||
"Protecting Routes\n",
|
||
"To protect a route so that it can only be accessed by authenticated users, Flask-Login\n",
|
||
"provides a login_required decorator. An example of its usage follows:\n",
|
||
"108 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"from flask_login import login_required\n",
|
||
"@app.route('/secret')\n",
|
||
"@login_required\n",
|
||
"def secret():\n",
|
||
" return 'Only authenticated users are allowed!'\n",
|
||
"You can see from this example that it is possible to “chain” multiple function decora‐\n",
|
||
"tors. When two or more decorators are added to a function, each decorator only\n",
|
||
"affects those that are below it, in addition to the target function. In this example, the\n",
|
||
"secret() \n",
|
||
"function \n",
|
||
"will \n",
|
||
"be \n",
|
||
"protected \n",
|
||
"against \n",
|
||
"unauthorized \n",
|
||
"users \n",
|
||
"with\n",
|
||
"login_required, and then the resulting function will be registered with Flask as a\n",
|
||
"route. Reversing the order will produce the wrong result, as the original function will\n",
|
||
"be registered as a route before it receives the additional properties from the\n",
|
||
"login_required decorator.\n",
|
||
"Thanks to the login_required decorator, if this route is accessed by a user who is not\n",
|
||
"authenticated, Flask-Login will intercept the request and send the user to the login\n",
|
||
"page instead.\n",
|
||
"Adding a Login Form\n",
|
||
"The login form that will be presented to users has a text field for the email address, a\n",
|
||
"password field, a “remember me” checkbox, and a submit button. The Flask-WTF\n",
|
||
"form class that defines this form is shown in Example 8-9.\n",
|
||
"Example 8-9. app/auth/forms.py: login form\n",
|
||
"from flask_wtf import FlaskForm\n",
|
||
"from wtforms import StringField, PasswordField, BooleanField, SubmitField\n",
|
||
"from wtforms.validators import DataRequired, Length, Email\n",
|
||
"class LoginForm(FlaskForm):\n",
|
||
" email = StringField('Email', validators=[DataRequired(), Length(1, 64),\n",
|
||
" Email()])\n",
|
||
" password = PasswordField('Password', validators=[DataRequired()])\n",
|
||
" remember_me = BooleanField('Keep me logged in')\n",
|
||
" submit = SubmitField('Log In')\n",
|
||
"The PasswordField class represents an <input> element with type=\"password\". The\n",
|
||
"BooleanField class represents a checkbox.\n",
|
||
"The email field uses the Length() and Email() validators from WTForms in addition\n",
|
||
"to DataRequired(), to ensure that the user not only provides a value for this field, but\n",
|
||
"that it is valid. When providing a list of validators, WTForms will evaluate them in\n",
|
||
"the order provided, and in case of a validation failure the error message shown will be\n",
|
||
"the one of the first validator that failed.\n",
|
||
"User Authentication with Flask-Login \n",
|
||
"| \n",
|
||
"109\n",
|
||
"\n",
|
||
"The template associated with the login page is stored in auth/login.html. This tem‐\n",
|
||
"plate just needs to render the form using Flask-Bootstrap’s wtf.quick_form() macro.\n",
|
||
"Figure 8-1 shows the login form rendered by the web browser.\n",
|
||
"Figure 8-1. Login form\n",
|
||
"The navigation bar in the base.html template uses a Jinja2 conditional to display “Log\n",
|
||
"In” or “Log Out” links depending on the logged-in state of the current user. The con‐\n",
|
||
"ditional is shown in Example 8-10.\n",
|
||
"Example 8-10. app/templates/base.html: Log In and Log Out navigation bar links\n",
|
||
"<ul class=\"nav navbar-nav navbar-right\">\n",
|
||
" {% if current_user.is_authenticated %}\n",
|
||
" <li><a href=\"{{ url_for('auth.logout') }}\">Log Out</a></li>\n",
|
||
" {% else %}\n",
|
||
" <li><a href=\"{{ url_for('auth.login') }}\">Log In</a></li>\n",
|
||
" {% endif %}\n",
|
||
"</ul>\n",
|
||
"The current_user variable used in the conditional is defined by Flask-Login and is\n",
|
||
"automatically available to view functions and templates. This variable contains the\n",
|
||
"currently logged-in user, or a proxy anonymous user object if the user is not logged\n",
|
||
"in. Anonymous user objects have the is_authenticated property set to False, so the\n",
|
||
"110 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"expression current_user.is_authenticated is a convenient way to know whether\n",
|
||
"the current user is logged in.\n",
|
||
"Signing Users In\n",
|
||
"The implementation of the login() view function is shown in Example 8-11.\n",
|
||
"Example 8-11. app/auth/views.py: login route\n",
|
||
"from flask import render_template, redirect, request, url_for, flash\n",
|
||
"from flask_login import login_user\n",
|
||
"from . import auth\n",
|
||
"from ..models import User\n",
|
||
"from .forms import LoginForm\n",
|
||
"@auth.route('/login', methods=['GET', 'POST'])\n",
|
||
"def login():\n",
|
||
" form = LoginForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" user = User.query.filter_by(email=form.email.data).first()\n",
|
||
" if user is not None and user.verify_password(form.password.data):\n",
|
||
" login_user(user, form.remember_me.data)\n",
|
||
" next = request.args.get('next')\n",
|
||
" if next is None or not next.startswith('/'):\n",
|
||
" next = url_for('main.index')\n",
|
||
" return redirect(next)\n",
|
||
" flash('Invalid username or password.')\n",
|
||
" return render_template('auth/login.html', form=form)\n",
|
||
"The view function creates a LoginForm object and uses it like the simple form in\n",
|
||
"Chapter 4. When the request is of type GET, the view function just renders the tem‐\n",
|
||
"plate, which in turn displays the form. When the form is submitted in a POST request,\n",
|
||
"Flask-WTF’s validate_on_submit() function validates the form variables, and then\n",
|
||
"attempts to log the user in.\n",
|
||
"To log a user in, the function begins by loading the user from the database using the\n",
|
||
"email provided with the form. If a user with the given email address exists, then its\n",
|
||
"verify_password() method is called with the password that also came with the form.\n",
|
||
"If the password is valid, Flask-Login’s login_user() function is invoked to record the\n",
|
||
"user as logged in for the user session. The login_user() function takes the user to\n",
|
||
"log in and an optional “remember me” Boolean, which was also submitted with the\n",
|
||
"form. A value of False for this argument causes the user session to expire when the\n",
|
||
"browser window is closed, so the user will have to log in again next time. A value of\n",
|
||
"True causes a long-term cookie to be set in the user’s browser, which Flask-Login uses\n",
|
||
"to restore the user session. The optional REMEMBER_COOKIE_DURATION configuration\n",
|
||
"option can be used to change the default one-year duration for the remember cookie.\n",
|
||
"User Authentication with Flask-Login \n",
|
||
"| \n",
|
||
"111\n",
|
||
"\n",
|
||
"In accordance with the Post/Redirect/Get pattern discussed in Chapter 4, the POST\n",
|
||
"request that submitted the login credentials ends with a redirect, but there are two\n",
|
||
"possible URL destinations. If the login form was presented to the user to prevent\n",
|
||
"unauthorized access to a protected URL the user wanted to visit, then Flask-Login\n",
|
||
"will have saved that original URL in the next query string argument, which can be\n",
|
||
"accessed from the request.args dictionary. If the next query string argument is not\n",
|
||
"available, a redirect to the home page is issued instead. The URL in next is validated\n",
|
||
"to make sure it is a relative URL, to prevent a malicious user from using this argu‐\n",
|
||
"ment to redirect unsuspecting users to another site.\n",
|
||
"For the case where the email address or password provided by the user is invalid, a\n",
|
||
"flash message is set and the form is rendered again for the user to retry.\n",
|
||
"On a production server, the application must be made available\n",
|
||
"over secure HTTP, so that login credentials and user sessions are\n",
|
||
"always transmitted encrypted. Without secure HTTP, sensitive data\n",
|
||
"can be intercepted during transit by an attacker.\n",
|
||
"The login template needs to be updated to render the form. These changes are shown\n",
|
||
"in Example 8-12.\n",
|
||
"Example 8-12. app/templates/auth/login.html: login form template\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% import \"bootstrap/wtf.html\" as wtf %}\n",
|
||
"{% block title %}Flasky - Login{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>Login</h1>\n",
|
||
"</div>\n",
|
||
"<div class=\"col-md-4\">\n",
|
||
" {{ wtf.quick_form(form) }}\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"Signing Users Out\n",
|
||
"The implementation of the logout route is shown in Example 8-13.\n",
|
||
"Example 8-13. app/auth/views.py: logout route\n",
|
||
"from flask_login import logout_user, login_required\n",
|
||
"@auth.route('/logout')\n",
|
||
"112 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"@login_required\n",
|
||
"def logout():\n",
|
||
" logout_user()\n",
|
||
" flash('You have been logged out.')\n",
|
||
" return redirect(url_for('main.index'))\n",
|
||
"To log a user out, Flask-Login’s logout_user() function is called to remove and reset\n",
|
||
"the user session. The logout is completed with a flash message that confirms the\n",
|
||
"action and a redirect to the home page.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 8c to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code. To ensure that\n",
|
||
"you have all the dependencies installed, also run pip install -r\n",
|
||
"requirements.txt.\n",
|
||
"Understanding How Flask-Login Works\n",
|
||
"Flask-Login is a fairly small extension, but due to the many moving pieces involved in\n",
|
||
"the authentication flow, Flask users often have trouble understanding how the exten‐\n",
|
||
"sion works. The following is the sequence of operations that occur when a user logs in\n",
|
||
"to the system:\n",
|
||
"1. The user navigates to http://localhost:5000/auth/login by clicking on the “Log In”\n",
|
||
"link. The handler for this URL returns the login form template.\n",
|
||
"2. The user enters their username and password, and presses the Submit button.\n",
|
||
"The same handler is invoked again, but now as a POST request instead of GET.\n",
|
||
"a. The handler validates the credentials submitted with the form, and then\n",
|
||
"invokes Flask-Login’s login_user() function to log the user in.\n",
|
||
"b. The login_user() function writes the ID of the user to the user session as a\n",
|
||
"string.\n",
|
||
"c. The view function returns with a redirect to the home page.\n",
|
||
"3. The browser receives the redirect and requests the home page.\n",
|
||
"a. The view function for the home page is invoked, and it triggers the rendering\n",
|
||
"of the main Jinja2 template.\n",
|
||
"b. During the rendering of the Jinja2 template, a reference to Flask-Login’s\n",
|
||
"current_user appears for the first time.\n",
|
||
"c. The current_user context variable does not have a value assigned for this\n",
|
||
"request yet, so it invokes Flask-Login’s internal function _get_user() to find\n",
|
||
"out who the user is.\n",
|
||
"User Authentication with Flask-Login \n",
|
||
"| \n",
|
||
"113\n",
|
||
"\n",
|
||
"d. The _get_user() function checks if there is a user ID stored in the user ses‐\n",
|
||
"sion. If there isn’t one, it returns an instance of Flask-Login’s AnonymousUser.\n",
|
||
"If there is an ID, it invokes the function that the application registered with\n",
|
||
"the user_loader decorator, with the ID as its argument.\n",
|
||
"e. The application’s user_loader handler reads the user from the database and\n",
|
||
"returns it. Flask-Login assigns it to the current_user context variable for the\n",
|
||
"current request.\n",
|
||
"f. The template receives the newly assigned value of current_user.\n",
|
||
"The login_required decorator builds on top of the current_user context variable\n",
|
||
"by only allowing the decorated view function to run when the expression\n",
|
||
"current_user.is_authenticated is True. The logout_user() function simply dele‐\n",
|
||
"tes the user ID from the user session.\n",
|
||
"Testing Logins\n",
|
||
"To verify that the login functionality is working, the home page can be updated to\n",
|
||
"greet the logged-in user by name. The template section that generates the greeting is\n",
|
||
"shown in Example 8-14.\n",
|
||
"Example 8-14. app/templates/index.html: greeting the logged-in user\n",
|
||
"Hello,\n",
|
||
"{% if current_user.is_authenticated %}\n",
|
||
" {{ current_user.username }}\n",
|
||
"{% else %}\n",
|
||
" Stranger\n",
|
||
"{% endif %}!\n",
|
||
"In this template once again current_user.is_authenticated is used to determine\n",
|
||
"whether the user is logged in.\n",
|
||
"Because no user registration functionality has been built, a new user can only be reg‐\n",
|
||
"istered from the shell at this time:\n",
|
||
"(venv) $ $ flask shell\n",
|
||
">>> u = User(email='john@example.com', username='john', password='cat')\n",
|
||
">>> db.session.add(u)\n",
|
||
">>> db.session.commit()\n",
|
||
"The user created previously can now log in. Figure 8-2 shows the application home\n",
|
||
"page with the user logged in.\n",
|
||
"114 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"Figure 8-2. Home page after successful login\n",
|
||
"New User Registration\n",
|
||
"When new users want to become members of the application, they must register with\n",
|
||
"it so that they are known and can log in. A link in the login page will send them to a\n",
|
||
"registration page, where they can enter their email address, username, and password.\n",
|
||
"Adding a User Registration Form\n",
|
||
"The form that will be used in the registration page asks the user to enter an email\n",
|
||
"address, username, and password. This form is shown in Example 8-15.\n",
|
||
"Example 8-15. app/auth/forms.py: user registration form\n",
|
||
"from flask_wtf import FlaskForm\n",
|
||
"from wtforms import StringField, PasswordField, BooleanField, SubmitField\n",
|
||
"from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo\n",
|
||
"from wtforms import ValidationError\n",
|
||
"from ..models import User\n",
|
||
"class RegistrationForm(FlaskForm):\n",
|
||
" email = StringField('Email', validators=[DataRequired(), Length(1, 64),\n",
|
||
" Email()])\n",
|
||
"New User Registration \n",
|
||
"| \n",
|
||
"115\n",
|
||
"\n",
|
||
" username = StringField('Username', validators=[\n",
|
||
" DataRequired(), Length(1, 64),\n",
|
||
" Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,\n",
|
||
" 'Usernames must have only letters, numbers, dots or '\n",
|
||
" 'underscores')])\n",
|
||
" password = PasswordField('Password', validators=[\n",
|
||
" DataRequired(), EqualTo('password2', message='Passwords must match.')])\n",
|
||
" password2 = PasswordField('Confirm password', validators=[DataRequired()])\n",
|
||
" submit = SubmitField('Register')\n",
|
||
" def validate_email(self, field):\n",
|
||
" if User.query.filter_by(email=field.data).first():\n",
|
||
" raise ValidationError('Email already registered.')\n",
|
||
" def validate_username(self, field):\n",
|
||
" if User.query.filter_by(username=field.data).first():\n",
|
||
" raise ValidationError('Username already in use.')\n",
|
||
"This form uses the Regexp validator from WTForms to ensure that the username field\n",
|
||
"starts with a letter and only contains letters, numbers, underscores, and dots. The two\n",
|
||
"arguments to the validator that follow the regular expression are the regular expres‐\n",
|
||
"sion flags and the error message to display on failure.\n",
|
||
"The password is entered twice as a safety measure, but this step makes it necessary to\n",
|
||
"validate that the two password fields have the same content, which is done with\n",
|
||
"another validator from WTForms called EqualTo. This validator is attached to one of\n",
|
||
"the password fields with the name of the other field given as an argument.\n",
|
||
"This form also has two custom validators implemented as methods. When a form\n",
|
||
"defines a method with the prefix validate_ followed by the name of a field, the\n",
|
||
"method is invoked in addition to any regularly defined validators. In this case, the\n",
|
||
"custom validators for email and username ensure that the values given are not dupli‐\n",
|
||
"cates. The custom validators indicate a validation error by raising a ValidationError\n",
|
||
"exception with the text of the error message as an argument.\n",
|
||
"The template that presents this form is called /templates/auth/register.html. Like the\n",
|
||
"login template, this one also renders the form with wtf.quick_form(). The registra‐\n",
|
||
"tion page is shown in Figure 8-3.\n",
|
||
"116 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"Figure 8-3. New user registration form\n",
|
||
"The registration page needs to be linked from the login page so that users who don’t\n",
|
||
"have an account can easily find it. This change is shown in Example 8-16.\n",
|
||
"Example 8-16. app/templates/auth/login.html: link to the registration page\n",
|
||
"<p>\n",
|
||
" New user?\n",
|
||
" <a href=\"{{ url_for('auth.register') }}\">\n",
|
||
" Click here to register\n",
|
||
" </a>\n",
|
||
"</p>\n",
|
||
"Registering New Users\n",
|
||
"Handling user registrations does not present any big surprises. When the registration\n",
|
||
"form is submitted and validated, a new user is added to the database using the infor‐\n",
|
||
"mation provided by the user. The view function that performs this task is shown in\n",
|
||
"Example 8-17.\n",
|
||
"New User Registration \n",
|
||
"| \n",
|
||
"117\n",
|
||
"\n",
|
||
"Example 8-17. app/auth/views.py: user registration route\n",
|
||
"@auth.route('/register', methods=['GET', 'POST'])\n",
|
||
"def register():\n",
|
||
" form = RegistrationForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" user = User(email=form.email.data,\n",
|
||
" username=form.username.data,\n",
|
||
" password=form.password.data)\n",
|
||
" db.session.add(user)\n",
|
||
" db.session.commit()\n",
|
||
" flash('You can now login.')\n",
|
||
" return redirect(url_for('auth.login'))\n",
|
||
" return render_template('auth/register.html', form=form)\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 8d to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Account Confirmation\n",
|
||
"For certain types of applications, it is important to ensure that the user information\n",
|
||
"provided during registration is valid. A common requirement is to ensure that the\n",
|
||
"user can be reached through the provided email address.\n",
|
||
"To validate the email address, applications send a confirmation email to users imme‐\n",
|
||
"diately after they register. The new account is initially marked as unconfirmed until\n",
|
||
"the instructions in the email are followed, which proves that the user has received the\n",
|
||
"email. The account confirmation procedure usually involves clicking a specially craf‐\n",
|
||
"ted URL link that includes a confirmation token.\n",
|
||
"Generating Confirmation Tokens with itsdangerous\n",
|
||
"The simplest account confirmation link would be a URL with the format http://\n",
|
||
"www.example.com/auth/confirm/<id> included in the confirmation email, where\n",
|
||
"<id> is the numeric id assigned to the user in the database. When the user clicks the\n",
|
||
"link, the view function that handles this route receives the user id to confirm as an\n",
|
||
"argument and can easily update the confirmed status of the user.\n",
|
||
"But this is obviously not a secure implementation, as any user who figures out the\n",
|
||
"format of the confirmation links will be able to confirm arbitrary accounts just by\n",
|
||
"sending random numbers in the URL. The idea is to replace the <id> in the URL\n",
|
||
"with a token that contains the same information, but in such a way that only the\n",
|
||
"server can generate valid confirmation URLs.\n",
|
||
"118 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"If you recall the discussion on user sessions in Chapter 4, Flask uses cryptographically\n",
|
||
"signed cookies to protect the content of user sessions against tampering. The user ses‐\n",
|
||
"sion cookies contain a cryptographic signature generated by a package called\n",
|
||
"itsdangerous. If the contents of the user session is altered, the signature will not\n",
|
||
"match the content anymore, so Flask discards the session and starts a new one. The\n",
|
||
"same concept can be applied to confirmation tokens.\n",
|
||
"The following is a short shell session that shows how itsdangerous can generate a\n",
|
||
"signed token that contains a user id inside:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer\n",
|
||
">>> s = Serializer(app.config['SECRET_KEY'], expires_in=3600)\n",
|
||
">>> token = s.dumps({ 'confirm': 23 })\n",
|
||
">>> token\n",
|
||
"'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...'\n",
|
||
">>> data = s.loads(token)\n",
|
||
">>> data\n",
|
||
"{'confirm': 23}\n",
|
||
"The itsdangerous package provides several types of token generators. Among them,\n",
|
||
"the class TimedJSONWebSignatureSerializer generates JSON Web Signatures (JWSs)\n",
|
||
"with a time expiration. The constructor of this class takes an encryption key as an\n",
|
||
"argument, which in a Flask application can be the configured SECRET_KEY.\n",
|
||
"The dumps() method generates a cryptographic signature for the data given as an\n",
|
||
"argument and then serializes the data plus the signature as a convenient token string.\n",
|
||
"The expires_in argument sets an expiration time for the token, expressed in sec‐\n",
|
||
"onds.\n",
|
||
"To decode the token, the serializer object provides a loads() method that takes the\n",
|
||
"token as its only argument. The function verifies the signature and the expiration\n",
|
||
"time and, if both are valid, it returns the original data. When the loads() method is\n",
|
||
"given an invalid token or a valid token that is expired, an exception is raised.\n",
|
||
"Token generation and verification using this functionality can be added to the User\n",
|
||
"model. The changes are shown in Example 8-18.\n",
|
||
"Example 8-18. app/models.py: user account confirmation\n",
|
||
"from itsdangerous import TimedJSONWebSignatureSerializer as Serializer\n",
|
||
"from flask import current_app\n",
|
||
"from . import db\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" confirmed = db.Column(db.Boolean, default=False)\n",
|
||
"Account Confirmation \n",
|
||
"| \n",
|
||
"119\n",
|
||
"\n",
|
||
" def generate_confirmation_token(self, expiration=3600):\n",
|
||
" s = Serializer(current_app.config['SECRET_KEY'], expiration)\n",
|
||
" return s.dumps({'confirm': self.id}).decode('utf-8')\n",
|
||
" def confirm(self, token):\n",
|
||
" s = Serializer(current_app.config['SECRET_KEY'])\n",
|
||
" try:\n",
|
||
" data = s.loads(token.encode('utf-8'))\n",
|
||
" except:\n",
|
||
" return False\n",
|
||
" if data.get('confirm') != self.id:\n",
|
||
" return False\n",
|
||
" self.confirmed = True\n",
|
||
" db.session.add(self)\n",
|
||
" return True\n",
|
||
"The generate_confirmation_token() method generates a token with a default val‐\n",
|
||
"idity time of one hour. The confirm() method verifies the token and, if valid, sets the\n",
|
||
"new confirmed attribute in the user model to True.\n",
|
||
"In addition to verifying the token, the confirm() function checks that the id from\n",
|
||
"the token matches the logged-in user, which is stored in current_user. This ensures\n",
|
||
"that a confirmation token for a given user cannot be used to confirm a different user.\n",
|
||
"Because a new column was added to the model to track the con‐\n",
|
||
"firmed state of each account, a new database migration needs to be\n",
|
||
"generated and applied.\n",
|
||
"The two new methods added to the User model are easily tested in unit tests. You can\n",
|
||
"find the unit tests in the GitHub repository for the application.\n",
|
||
"Sending Confirmation Emails\n",
|
||
"The current /register route redirects to /index after adding the new user to the data‐\n",
|
||
"base. Before redirecting, this route now needs to send the confirmation email. This\n",
|
||
"change is shown in Example 8-19.\n",
|
||
"Example 8-19. app/auth/views.py: registration route with confirmation email\n",
|
||
"from ..email import send_email\n",
|
||
"@auth.route('/register', methods=['GET', 'POST'])\n",
|
||
"def register():\n",
|
||
" form = RegistrationForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" # ...\n",
|
||
"120 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
" db.session.add(user)\n",
|
||
" db.session.commit()\n",
|
||
" token = user.generate_confirmation_token()\n",
|
||
" send_email(user.email, 'Confirm Your Account',\n",
|
||
" 'auth/email/confirm', user=user, token=token)\n",
|
||
" flash('A confirmation email has been sent to you by email.')\n",
|
||
" return redirect(url_for('main.index'))\n",
|
||
" return render_template('auth/register.html', form=form)\n",
|
||
"Note that a db.session.commit() call had to be added before the confirmation email\n",
|
||
"is sent out. The problem is that new users get assigned an id when they are commit‐\n",
|
||
"ted to the database, and this id is needed to generate the confirmation token.\n",
|
||
"The email templates used by the authentication blueprint will be added in the tem‐\n",
|
||
"plates/auth/email directory to keep them separate from the HTML templates. As dis‐\n",
|
||
"cussed in Chapter 6, for each email two templates are needed for the plain-text and\n",
|
||
"HTML versions of the body. As an example, Example 8-20 shows the plain-text ver‐\n",
|
||
"sion of the confirmation email template, and you can find the equivalent HTML ver‐\n",
|
||
"sion in the GitHub repository.\n",
|
||
"Example 8-20. app/templates/auth/email/confirm.txt: text body of confirmation email\n",
|
||
"Dear {{ user.username }},\n",
|
||
"Welcome to Flasky!\n",
|
||
"To confirm your account please click on the following link:\n",
|
||
"{{ url_for('auth.confirm', token=token, _external=True) }}\n",
|
||
"Sincerely,\n",
|
||
"The Flasky Team\n",
|
||
"Note: replies to this email address are not monitored.\n",
|
||
"By \n",
|
||
"default, \n",
|
||
"url_for() \n",
|
||
"generates \n",
|
||
"relative \n",
|
||
"URLs; \n",
|
||
"so, \n",
|
||
"for \n",
|
||
"example,\n",
|
||
"url_for('auth.confirm', token='abc') returns the string '/auth/confirm/abc'.\n",
|
||
"This, of course, is not a valid URL that can be sent in an email, since it is only the\n",
|
||
"path portion of the URL. Relative URLs work fine when they are used within the con‐\n",
|
||
"text of a web page because the browser converts them to absolute URLs by adding the\n",
|
||
"hostname and port number from the current page, but when sending a URL over\n",
|
||
"email there is no such context. The _external=True argument is added to the\n",
|
||
"url_for() call to request a fully qualified URL that includes the scheme (http:// or\n",
|
||
"https://), hostname, and port.\n",
|
||
"The view function that confirms accounts is shown in Example 8-21.\n",
|
||
"Account Confirmation \n",
|
||
"| \n",
|
||
"121\n",
|
||
"\n",
|
||
"Example 8-21. app/auth/views.py: confirming a user account\n",
|
||
"from flask_login import current_user\n",
|
||
"@auth.route('/confirm/<token>')\n",
|
||
"@login_required\n",
|
||
"def confirm(token):\n",
|
||
" if current_user.confirmed:\n",
|
||
" return redirect(url_for('main.index'))\n",
|
||
" if current_user.confirm(token):\n",
|
||
" db.session.commit()\n",
|
||
" flash('You have confirmed your account. Thanks!')\n",
|
||
" else:\n",
|
||
" flash('The confirmation link is invalid or has expired.')\n",
|
||
" return redirect(url_for('main.index'))\n",
|
||
"This route is protected with the login_required decorator from Flask-Login, so that\n",
|
||
"when the users click on the link from the confirmation email they are asked to log in\n",
|
||
"before they reach this view function.\n",
|
||
"The function first checks if the logged-in user is already confirmed, and in that case it\n",
|
||
"redirects to the home page, as obviously there is nothing to do. This can prevent\n",
|
||
"unnecessary work if a user clicks the confirmation token multiple times by mistake.\n",
|
||
"Because the actual token confirmation is done entirely in the User model, all the view\n",
|
||
"function needs to do is call the confirm() method and then flash a message accord‐\n",
|
||
"ing to the result. When the confirmation succeeds, the User model’s confirmed\n",
|
||
"attribute is changed and added to the session and then the database session is com‐\n",
|
||
"mitted.\n",
|
||
"Each application can decide what unconfirmed users are allowed to do before they\n",
|
||
"confirm their accounts. One possibility is to allow unconfirmed users to log in, but\n",
|
||
"only show them a page that asks them to confirm their accounts before they can gain\n",
|
||
"further access.\n",
|
||
"This step can be done using Flask’s before_request hook, which was briefly\n",
|
||
"described in Chapter 2. From a blueprint, the before_request hook applies only to\n",
|
||
"requests that belong to the blueprint. To install a blueprint hook for all application\n",
|
||
"requests, the before_app_request decorator must be used instead. Example 8-22\n",
|
||
"shows how this handler is implemented.\n",
|
||
"122 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"Example 8-22. app/auth/views.py: filtering unconfirmed accounts with the\n",
|
||
"before_app_request handler\n",
|
||
"@auth.before_app_request\n",
|
||
"def before_request():\n",
|
||
" if current_user.is_authenticated \\\n",
|
||
" and not current_user.confirmed \\\n",
|
||
" and request.blueprint != 'auth' \\\n",
|
||
" and request.endpoint != 'static':\n",
|
||
" return redirect(url_for('auth.unconfirmed'))\n",
|
||
"@auth.route('/unconfirmed')\n",
|
||
"def unconfirmed():\n",
|
||
" if current_user.is_anonymous or current_user.confirmed:\n",
|
||
" return redirect(url_for('main.index'))\n",
|
||
" return render_template('auth/unconfirmed.html')\n",
|
||
"The before_app_request handler will intercept a request when three conditions are\n",
|
||
"true:\n",
|
||
"1. A user is logged in (current_user.is_authenticated is True).\n",
|
||
"2. The account for the user is not confirmed.\n",
|
||
"3. The requested URL is outside of the authentication blueprint and is not for a\n",
|
||
"static file. Access to the authentication routes needs to be granted, as those are the\n",
|
||
"routes that will enable the user to confirm the account or perform other account\n",
|
||
"management functions.\n",
|
||
"If these three conditions are met, then a redirect is issued to a new /auth/unconfirmed\n",
|
||
"route that shows a page with information about account confirmation.\n",
|
||
"When a before_request or before_app_request callback returns\n",
|
||
"a response or a redirect, Flask sends that to the client without\n",
|
||
"invoking the view function associated with the request. This effec‐\n",
|
||
"tively allows these callbacks to intercept a request when necessary.\n",
|
||
"The page that is presented to unconfirmed users (shown in Figure 8-4) just renders a\n",
|
||
"template that gives users instructions for how to confirm their accounts and offers a\n",
|
||
"link to request a new confirmation email, in case the original email was lost. The\n",
|
||
"route that resends the confirmation email is shown in Example 8-23.\n",
|
||
"Account Confirmation \n",
|
||
"| \n",
|
||
"123\n",
|
||
"\n",
|
||
"Figure 8-4. Unconfirmed account page\n",
|
||
"Example 8-23. app/auth/views.py: resending the account confirmation email\n",
|
||
"@auth.route('/confirm')\n",
|
||
"@login_required\n",
|
||
"def resend_confirmation():\n",
|
||
" token = current_user.generate_confirmation_token()\n",
|
||
" send_email(current_user.email, 'Confirm Your Account',\n",
|
||
" 'auth/email/confirm', user=current_user, token=token)\n",
|
||
" flash('A new confirmation email has been sent to you by email.')\n",
|
||
" return redirect(url_for('main.index'))\n",
|
||
"This route repeats what was done in the registration route using current_user, the\n",
|
||
"user who is logged in, as the target user. This route is also protected with\n",
|
||
"login_required to ensure that when it is accessed, the user that is making the\n",
|
||
"request is authenticated.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 8e to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code.\n",
|
||
"124 \n",
|
||
"| \n",
|
||
"Chapter 8: User Authentication\n",
|
||
"\n",
|
||
"Account Management\n",
|
||
"Users who have accounts with the application may need to make changes to their\n",
|
||
"accounts from time to time. The following tasks can be added to the authentication\n",
|
||
"blueprint using the techniques presented in this chapter:\n",
|
||
"Password updates\n",
|
||
"Security-conscious users may want to change their passwords periodically. This is\n",
|
||
"an easy feature to implement, because as long as the user is logged in, it is safe to\n",
|
||
"present a form that asks for the old password and a new password to replace it.\n",
|
||
"This feature is implemented as commit 8f in the GitHub repository. As part of\n",
|
||
"this change, the “Log Out” link in the navigation bar was refactored into a drop‐\n",
|
||
"down that contains the “Change Password” and “Log Out” links.\n",
|
||
"Password resets\n",
|
||
"To avoid locking users out of the application when they forget their passwords, a\n",
|
||
"password reset option can be offered. To implement password resets in a secure\n",
|
||
"way, it is necessary to use tokens similar to those used to confirm accounts.\n",
|
||
"When a user requests a password reset, an email with a reset token is sent to the\n",
|
||
"registered email address. The user then clicks the link in the email and, after the\n",
|
||
"token is verified, a form is presented where a new password can be entered. This\n",
|
||
"feature is implemented as commit 8g in the GitHub repository.\n",
|
||
"Email address changes\n",
|
||
"Users can be given the option to change their registered email address, but before\n",
|
||
"the new address is accepted it must be verified with a confirmation email. To use\n",
|
||
"this feature, the user enters the new email address in a form. To confirm the\n",
|
||
"email address, a token is emailed to that address. When the server receives the\n",
|
||
"token back, it can update the user object. While the server waits to receive the\n",
|
||
"token, it can store the new email address in a new database field reserved for\n",
|
||
"pending email addresses, or it can store the address in the token along with the\n",
|
||
"id. This feature is implemented as commit 8h in the GitHub repository.\n",
|
||
"In the next chapter, the user subsystem of Flasky will be extended through the use of\n",
|
||
"user roles.\n",
|
||
"Account Management \n",
|
||
"| \n",
|
||
"125\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 9\n",
|
||
"User Roles\n",
|
||
"Not all users of web applications are created equal. In most applications, a small per‐\n",
|
||
"centage of users are trusted with extra powers to help keep the application running\n",
|
||
"smoothly. Administrators are the best example, but in many cases middle-level power\n",
|
||
"users such as content moderators exist as well. To implement this, all users are\n",
|
||
"assigned a role.\n",
|
||
"There are several ways to implement roles in an application. The appropriate method\n",
|
||
"largely depends on how many roles need to be supported and how elaborate they are.\n",
|
||
"For example, a simple application may need just two roles, one for regular users and\n",
|
||
"one for administrators. In this case, having an is_administrator Boolean field in the\n",
|
||
"User model may be all that is necessary. A more complex application may need addi‐\n",
|
||
"tional roles with varying levels of power in between regular users and administrators.\n",
|
||
"In some applications it may not even make sense to talk about discrete roles, and\n",
|
||
"instead giving users a set of individual permissions may be the right approach.\n",
|
||
"The user role implementation presented in this chapter is a hybrid between discrete\n",
|
||
"roles and permissions. Users are assigned a discrete role, but each role defines what\n",
|
||
"actions it allows its users to perform through a list of permissions.\n",
|
||
"Database Representation of Roles\n",
|
||
"A simple roles table was created in Chapter 5 as a vehicle to demonstrate one-to-\n",
|
||
"many relationships. Example 9-1 shows an improved Role model with some addi‐\n",
|
||
"tions.\n",
|
||
"127\n",
|
||
"\n",
|
||
"Example 9-1. app/models.py: role database model\n",
|
||
"class Role(db.Model):\n",
|
||
" __tablename__ = 'roles'\n",
|
||
" id = db.Column(db.Integer, primary_key=True)\n",
|
||
" name = db.Column(db.String(64), unique=True)\n",
|
||
" default = db.Column(db.Boolean, default=False, index=True)\n",
|
||
" permissions = db.Column(db.Integer)\n",
|
||
" users = db.relationship('User', backref='role', lazy='dynamic')\n",
|
||
" def __init__(self, **kwargs):\n",
|
||
" super(Role, self).__init__(**kwargs)\n",
|
||
" if self.permissions is None:\n",
|
||
" self.permissions = 0\n",
|
||
"The default field is one of the additions to this model. This field should be set to\n",
|
||
"True for only one role and False for all the others. The role marked as default will be\n",
|
||
"the one assigned to new users upon registration. Since the application is going to\n",
|
||
"search the roles table to find the default one, this column is configured to have an\n",
|
||
"index, as that will make searches much faster.\n",
|
||
"Another addition to the model is the permissions field, which is an integer value that\n",
|
||
"defines the list of permissions for the role in a compact way. Since SQLAlchemy will\n",
|
||
"set this field to None by default, a class constructor is added that sets it to 0 if an initial\n",
|
||
"value isn’t provided in the constructor arguments.\n",
|
||
"The list of tasks for which permissions are needed is obviously application specific.\n",
|
||
"For Flasky, the list is shown in Table 9-1.\n",
|
||
"Table 9-1. Application permissions\n",
|
||
"Task name\n",
|
||
"Permission name Permission value\n",
|
||
"Follow users\n",
|
||
"FOLLOW\n",
|
||
"1\n",
|
||
"Comment on posts made by others\n",
|
||
"COMMENT\n",
|
||
"2\n",
|
||
"Write articles\n",
|
||
"WRITE\n",
|
||
"4\n",
|
||
"Moderate comments made by others\n",
|
||
"MODERATE\n",
|
||
"8\n",
|
||
"Administration access\n",
|
||
"ADMIN\n",
|
||
"16\n",
|
||
"The benefit of using powers of two for permission values is that it allows permissions\n",
|
||
"to be combined, giving each possible combination of permissions a unique value to\n",
|
||
"store in the role’s permissions field. For example, for a user role that gives users per‐\n",
|
||
"mission to follow other users and comment on posts, the permission value is FOLLOW\n",
|
||
"+ COMMENT = 3. This is a very efficient way to store the list of permissions assigned to\n",
|
||
"each role.\n",
|
||
"128 \n",
|
||
"| \n",
|
||
"Chapter 9: User Roles\n",
|
||
"\n",
|
||
"The code representation of Table 9-1 is shown in Example 9-2.\n",
|
||
"Example 9-2. app/models.py: permission constants\n",
|
||
"class Permission:\n",
|
||
" FOLLOW = 1\n",
|
||
" COMMENT = 2\n",
|
||
" WRITE = 4\n",
|
||
" MODERATE = 8\n",
|
||
" ADMIN = 16\n",
|
||
"With the permission constants in place, a few new methods can be added to the Role\n",
|
||
"model to manage permissions. These are shown in Example 9-3.\n",
|
||
"Example 9-3. app/models.py: permission management in the Role model\n",
|
||
"class Role(db.Model):\n",
|
||
" # ...\n",
|
||
" def add_permission(self, perm):\n",
|
||
" if not self.has_permission(perm):\n",
|
||
" self.permissions += perm\n",
|
||
" def remove_permission(self, perm):\n",
|
||
" if self.has_permission(perm):\n",
|
||
" self.permissions -= perm\n",
|
||
" def reset_permissions(self):\n",
|
||
" self.permissions = 0\n",
|
||
" def has_permission(self, perm):\n",
|
||
" return self.permissions & perm == perm\n",
|
||
"The add_permission(), remove_permission(), and reset_permission() methods\n",
|
||
"all use basic arithmetic operations to update the permission list. The\n",
|
||
"has_permission() method is the most complex of the set, as it relies on the bitwise\n",
|
||
"and operator & to check if a combined permission value includes the given basic per‐\n",
|
||
"mission. You can play with these methods in a Python shell:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> r = Role(name='User')\n",
|
||
">>> r.add_permission(Permission.FOLLOW)\n",
|
||
">>> r.add_permission(Permission.WRITE)\n",
|
||
">>> r.has_permission(Permission.FOLLOW)\n",
|
||
"True\n",
|
||
">>> r.has_permission(Permission.ADMIN)\n",
|
||
"False\n",
|
||
">>> r.reset_permissions()\n",
|
||
"Database Representation of Roles \n",
|
||
"| \n",
|
||
"129\n",
|
||
"\n",
|
||
">>> r.has_permission(Permission.FOLLOW)\n",
|
||
"False\n",
|
||
"Table 9-2 shows the list of user roles that will be supported in this application, along\n",
|
||
"with the permission combinations that define each of them.\n",
|
||
"Table 9-2. User roles\n",
|
||
"User role\n",
|
||
"Permissions\n",
|
||
"Description\n",
|
||
"None\n",
|
||
"None\n",
|
||
"Read-only access to the application. This applies to unknown users who are not\n",
|
||
"logged in.\n",
|
||
"User\n",
|
||
"FOLLOW, COMMENT,\n",
|
||
"WRITE\n",
|
||
"Basic permissions to write articles and comments and to follow other users.\n",
|
||
"This is the default for new users.\n",
|
||
"Moderator\n",
|
||
"FOLLOW, COMMENT,\n",
|
||
"WRITE, MODERATE\n",
|
||
"Adds permission to moderate comments made by other users.\n",
|
||
"Administrator\n",
|
||
"FOLLOW, COMMENT,\n",
|
||
"WRITE, MODERATE,\n",
|
||
"ADMIN\n",
|
||
"Full access, which includes permission to change the roles of other users.\n",
|
||
"Adding the roles to the database manually is time consuming and error prone, so\n",
|
||
"instead a class method can be added to the Role class for this purpose, as shown in\n",
|
||
"Example 9-4. This will make it easy to re-create the correct roles and permissions\n",
|
||
"during unit testing and, more importantly, on the production server once the applica‐\n",
|
||
"tion is deployed.\n",
|
||
"Example 9-4. app/models.py: creating roles in the database\n",
|
||
"class Role(db.Model):\n",
|
||
" # ...\n",
|
||
" @staticmethod\n",
|
||
" def insert_roles():\n",
|
||
" roles = {\n",
|
||
" 'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],\n",
|
||
" 'Moderator': [Permission.FOLLOW, Permission.COMMENT,\n",
|
||
" Permission.WRITE, Permission.MODERATE],\n",
|
||
" 'Administrator': [Permission.FOLLOW, Permission.COMMENT,\n",
|
||
" Permission.WRITE, Permission.MODERATE,\n",
|
||
" Permission.ADMIN],\n",
|
||
" }\n",
|
||
" default_role = 'User'\n",
|
||
" for r in roles:\n",
|
||
" role = Role.query.filter_by(name=r).first()\n",
|
||
" if role is None:\n",
|
||
" role = Role(name=r)\n",
|
||
" role.reset_permissions()\n",
|
||
" for perm in roles[r]:\n",
|
||
" role.add_permission(perm)\n",
|
||
" role.default = (role.name == default_role)\n",
|
||
"130 \n",
|
||
"| \n",
|
||
"Chapter 9: User Roles\n",
|
||
"\n",
|
||
" db.session.add(role)\n",
|
||
" db.session.commit()\n",
|
||
"The insert_roles() function does not directly create new role objects. Instead, it\n",
|
||
"tries to find existing roles by name and update those. A new role object is created\n",
|
||
"only for roles that aren’t in the database already. This is done so that the role list can\n",
|
||
"be updated in the future when changes need to be made. To add a new role or change\n",
|
||
"the permission assignments for a role, change the roles dictionary at the top of the\n",
|
||
"function and then run the function again. Note that the \"Anonymous\" role does not\n",
|
||
"need to be represented in the database, as it is the role that represents users who are\n",
|
||
"not known and therefore are not in the database.\n",
|
||
"Note also that insert_roles() is a static method, a special type of method that does\n",
|
||
"not require an object to be created as it can be invoked directly on the class, for exam‐\n",
|
||
"ple, as Role.insert_roles(). Static methods do not take a self argument like\n",
|
||
"instance methods.\n",
|
||
"Role Assignment\n",
|
||
"When users register an account with the application, the correct role should be\n",
|
||
"assigned to them. For most users, the role assigned at registration time will be the\n",
|
||
"\"User\" role, as that is the role that is marked as a default. The only exception is made\n",
|
||
"for the administrator, who needs to be assigned the \"Administrator\" role from the\n",
|
||
"start. This user is identified by an email address stored in the FLASKY_ADMIN configu‐\n",
|
||
"ration variable, so as soon as that email address appears in a registration request it\n",
|
||
"can be given the correct role. Example 9-5 shows how this is done in the User model\n",
|
||
"constructor.\n",
|
||
"Example 9-5. app/models.py: defining a default role for users\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" def __init__(self, **kwargs):\n",
|
||
" super(User, self).__init__(**kwargs)\n",
|
||
" if self.role is None:\n",
|
||
" if self.email == current_app.config['FLASKY_ADMIN']:\n",
|
||
" self.role = Role.query.filter_by(name='Administrator').first()\n",
|
||
" if self.role is None:\n",
|
||
" self.role = Role.query.filter_by(default=True).first()\n",
|
||
" # ...\n",
|
||
"The User constructor first invokes the constructors of the base classes, and if after\n",
|
||
"that the object does not have a role defined, it sets the administrator or default role\n",
|
||
"depending on the email address.\n",
|
||
"Role Assignment \n",
|
||
"| \n",
|
||
"131\n",
|
||
"\n",
|
||
"Role Verification\n",
|
||
"To simplify the implementation of roles and permissions, a helper method can be\n",
|
||
"added to the User model that checks whether users have a given permission in the\n",
|
||
"role they have been assigned. The implementation simply defers to the role methods\n",
|
||
"added previously. This is shown in Example 9-6.\n",
|
||
"Example 9-6. app/models.py: evaluating whether a user has a given permission\n",
|
||
"from flask_login import UserMixin, AnonymousUserMixin\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" def can(self, perm):\n",
|
||
" return self.role is not None and self.role.has_permission(perm)\n",
|
||
" def is_administrator(self):\n",
|
||
" return self.can(Permission.ADMIN)\n",
|
||
"class AnonymousUser(AnonymousUserMixin):\n",
|
||
" def can(self, permissions):\n",
|
||
" return False\n",
|
||
" def is_administrator(self):\n",
|
||
" return False\n",
|
||
"login_manager.anonymous_user = AnonymousUser\n",
|
||
"The can() method added to the User model returns True if the requested permission\n",
|
||
"is present in the role, which means that the user should be allowed to perform the\n",
|
||
"requested task. The check for administration permissions is so common that it is also\n",
|
||
"implemented as a standalone is_administrator() method.\n",
|
||
"For added convenience, a custom AnonymousUser class that implements the can()\n",
|
||
"and is_administrator() methods is created as well. This will enable the application\n",
|
||
"to freely call current_user.can() and current_user.is_administrator() without\n",
|
||
"having to check whether the user is logged in first. Flask-Login is told to use the\n",
|
||
"application’s \n",
|
||
"custom \n",
|
||
"anonymous \n",
|
||
"user \n",
|
||
"by \n",
|
||
"setting \n",
|
||
"its \n",
|
||
"class \n",
|
||
"in \n",
|
||
"the\n",
|
||
"login_manager.anonymous_user attribute.\n",
|
||
"For cases in which an entire view function needs to be made available only to users\n",
|
||
"with certain permissions, a custom decorator can be used. Example 9-7 shows the\n",
|
||
"implementation of two decorators, one for generic permission checks and one that\n",
|
||
"checks specifically for the administrator permission.\n",
|
||
"132 \n",
|
||
"| \n",
|
||
"Chapter 9: User Roles\n",
|
||
"\n",
|
||
"Example 9-7. app/decorators.py: custom decorators that check user permissions\n",
|
||
"from functools import wraps\n",
|
||
"from flask import abort\n",
|
||
"from flask_login import current_user\n",
|
||
"from .models import Permission\n",
|
||
"def permission_required(permission):\n",
|
||
" def decorator(f):\n",
|
||
" @wraps(f)\n",
|
||
" def decorated_function(*args, **kwargs):\n",
|
||
" if not current_user.can(permission):\n",
|
||
" abort(403)\n",
|
||
" return f(*args, **kwargs)\n",
|
||
" return decorated_function\n",
|
||
" return decorator\n",
|
||
"def admin_required(f):\n",
|
||
" return permission_required(Permission.ADMIN)(f)\n",
|
||
"These decorators are built with the help of the functools package from the Python\n",
|
||
"standard library and return a 403 response, the “Forbidden” HTTP status code, when\n",
|
||
"the current user does not have the requested permission. In Chapter 3, custom error\n",
|
||
"pages were created for errors 404 and 500, so now a page for the 403 error is added in\n",
|
||
"a similar way.\n",
|
||
"The following are two examples that demonstrate the usage of these decorators:\n",
|
||
"from .decorators import admin_required, permission_required\n",
|
||
"@main.route('/admin')\n",
|
||
"@login_required\n",
|
||
"@admin_required\n",
|
||
"def for_admins_only():\n",
|
||
" return \"For administrators!\"\n",
|
||
"@main.route('/moderate')\n",
|
||
"@login_required\n",
|
||
"@permission_required(Permission.MODERATE)\n",
|
||
"def for_moderators_only():\n",
|
||
" return \"For comment moderators!\"\n",
|
||
"As a rule of thumb, the route decorator from Flask should be given first when using\n",
|
||
"multiple decorators in a view function. The remaining decorators should be given in\n",
|
||
"the order in which they need to evaluate when the view function is invoked. In these\n",
|
||
"two cases, the user authenticated state needs to be checked first, since the user needs\n",
|
||
"to be redirected to the login prompt if found to not be authenticated.\n",
|
||
"Permissions may also need to be checked from templates, so the Permission class\n",
|
||
"with all its constants needs to be accessible to them. To avoid having to add a tem‐\n",
|
||
"Role Verification \n",
|
||
"| \n",
|
||
"133\n",
|
||
"\n",
|
||
"plate argument in every render_template() call, a context processor can be used.\n",
|
||
"Context processors make variables available to all templates during rendering. This\n",
|
||
"change is shown in Example 9-8.\n",
|
||
"Example 9-8. app/main/__init__.py: adding the Permission class to the template context\n",
|
||
"@main.app_context_processor\n",
|
||
"def inject_permissions():\n",
|
||
" return dict(Permission=Permission)\n",
|
||
"The new roles and permissions can be exercised in unit tests. Example 9-9 shows two\n",
|
||
"of the tests. The source code on GitHub includes one for each role.\n",
|
||
"Example 9-9. tests/test_user_model.py: unit tests for roles and permissions\n",
|
||
"class UserModelTestCase(unittest.TestCase):\n",
|
||
" # ...\n",
|
||
" def test_user_role(self):\n",
|
||
" u = User(email='john@example.com', password='cat')\n",
|
||
" self.assertTrue(u.can(Permission.FOLLOW))\n",
|
||
" self.assertTrue(u.can(Permission.COMMENT))\n",
|
||
" self.assertTrue(u.can(Permission.WRITE))\n",
|
||
" self.assertFalse(u.can(Permission.MODERATE))\n",
|
||
" self.assertFalse(u.can(Permission.ADMIN))\n",
|
||
" def test_anonymous_user(self):\n",
|
||
" u = AnonymousUser()\n",
|
||
" self.assertFalse(u.can(Permission.FOLLOW))\n",
|
||
" self.assertFalse(u.can(Permission.COMMENT))\n",
|
||
" self.assertFalse(u.can(Permission.WRITE))\n",
|
||
" self.assertFalse(u.can(Permission.MODERATE))\n",
|
||
" self.assertFalse(u.can(Permission.ADMIN))\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 9a to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code.\n",
|
||
"Before you move on to the next chapter, add the new roles to your development data‐\n",
|
||
"base in a shell session:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> Role.insert_roles()\n",
|
||
">>> Role.query.all()\n",
|
||
"[<Role 'Administrator'>, <Role 'User'>, <Role 'Moderator'>]\n",
|
||
"134 \n",
|
||
"| \n",
|
||
"Chapter 9: User Roles\n",
|
||
"\n",
|
||
"It is also a good idea to update the user list so that all the user accounts that were\n",
|
||
"created before roles and permissions existed have a role assigned. You can run the fol‐\n",
|
||
"lowing code in a Python shell to perform this update:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> admin_role = Role.query.filter_by(name='Administrator').first()\n",
|
||
">>> default_role = Role.query.filter_by(default=True).first()\n",
|
||
">>> for u in User.query.all():\n",
|
||
"... if u.role is None:\n",
|
||
"... if u.email == app.config['FLASKY_ADMIN']:\n",
|
||
"... u.role = admin_role\n",
|
||
"... else:\n",
|
||
"... u.role = default_role\n",
|
||
"...\n",
|
||
">>> db.session.commit()\n",
|
||
"The user system is now fairly complete. The next chapter will make use of it to create\n",
|
||
"user profile pages.\n",
|
||
"Role Verification \n",
|
||
"| \n",
|
||
"135\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 10\n",
|
||
"User Profiles\n",
|
||
"In this chapter, user profiles for Flasky are implemented. All socially aware sites give\n",
|
||
"their users a profile page, where a summary of the user’s participation in the website\n",
|
||
"is presented. Users can advertise their presence on the website by sharing the URL to\n",
|
||
"their profile page, so it is important that the URLs be short and easy to remember.\n",
|
||
"Profile Information\n",
|
||
"To make user profile pages more interesting, some additional information about\n",
|
||
"users can be stored in the database. In Example 10-1 the User model is extended with\n",
|
||
"several new fields.\n",
|
||
"Example 10-1. app/models.py: user information fields\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" name = db.Column(db.String(64))\n",
|
||
" location = db.Column(db.String(64))\n",
|
||
" about_me = db.Column(db.Text())\n",
|
||
" member_since = db.Column(db.DateTime(), default=datetime.utcnow)\n",
|
||
" last_seen = db.Column(db.DateTime(), default=datetime.utcnow)\n",
|
||
"The new fields store the user’s real name, location, self-written bio, date of registra‐\n",
|
||
"tion, and date of last visit. The about_me field is assigned the type db.Text(). The\n",
|
||
"difference between db.String and db.Text is that db.Text is a variable-length field\n",
|
||
"and as such does not need a maximum length.\n",
|
||
"The two timestamps are given a default value of the current time. Note that the\n",
|
||
"datetime.utcnow is missing the () at the end. This is because the default argument\n",
|
||
"in db.Column() can take a function as a value. Each time a default value needs to be\n",
|
||
"137\n",
|
||
"\n",
|
||
"generated, SQLAlchemy invokes the function to produce it. This default value is all\n",
|
||
"that is needed to manage the member_since field.\n",
|
||
"The last_seen field is also initialized to the current time upon creation, but it needs\n",
|
||
"to be refreshed each time the user accesses the site. A method in the User class can be\n",
|
||
"added to perform this update. This is shown in Example 10-2.\n",
|
||
"Example 10-2. app/models.py: refreshing a user’s last visit time\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" def ping(self):\n",
|
||
" self.last_seen = datetime.utcnow()\n",
|
||
" db.session.add(self)\n",
|
||
" db.session.commit()\n",
|
||
"To keep the last visit date for all users updated, the ping() method must be called\n",
|
||
"each time a request from a user is received. Because the before_app_request handler \n",
|
||
"in the auth blueprint runs before every request, it can do this easily, as shown in\n",
|
||
"Example 10-3.\n",
|
||
"Example 10-3. app/auth/views.py: pinging the logged-in user\n",
|
||
"@auth.before_app_request\n",
|
||
"def before_request():\n",
|
||
" if current_user.is_authenticated:\n",
|
||
" current_user.ping()\n",
|
||
" if not current_user.confirmed \\\n",
|
||
" and request.endpoint \\\n",
|
||
" and request.blueprint != 'auth' \\\n",
|
||
" and request.endpoint != 'static':\n",
|
||
" return redirect(url_for('auth.unconfirmed'))\n",
|
||
"User Profile Page\n",
|
||
"Creating a profile page for each user does not present any new challenges.\n",
|
||
"Example 10-4 shows the route definition.\n",
|
||
"Example 10-4. app/main/views.py: profile page route\n",
|
||
"@main.route('/user/<username>')\n",
|
||
"def user(username):\n",
|
||
" user = User.query.filter_by(username=username).first_or_404()\n",
|
||
" return render_template('user.html', user=user)\n",
|
||
"138 \n",
|
||
"| \n",
|
||
"Chapter 10: User Profiles\n",
|
||
"\n",
|
||
"This route is added in the main blueprint. For a user named john, the profile page will\n",
|
||
"be at http://localhost:5000/user/john. The username given in the URL is searched in\n",
|
||
"the database and, if found, the user.html template is rendered with it as the argument.\n",
|
||
"An invalid username sent into this route will cause a 404 error to be returned. With\n",
|
||
"Flask-SQLAlchemy, the search and error cases can be nicely combined in a single\n",
|
||
"statement using the first_or_404() method of the query object. The user.html tem‐\n",
|
||
"plate is going to need to present user information, so it receives the user object as an\n",
|
||
"argument. An initial version of this template is shown in Example 10-5.\n",
|
||
"Example 10-5. app/templates/user.html: user profile template\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% block title %}Flasky - {{ user.username }}{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>{{ user.username }}</h1>\n",
|
||
" {% if user.name or user.location %}\n",
|
||
" <p>\n",
|
||
" {% if user.name %}{{ user.name }}{% endif %}\n",
|
||
" {% if user.location %}\n",
|
||
" From <a href=\"http://maps.google.com/?q={{ user.location }}\">\n",
|
||
" {{ user.location }}\n",
|
||
" </a>\n",
|
||
" {% endif %}\n",
|
||
" </p>\n",
|
||
" {% endif %}\n",
|
||
" {% if current_user.is_administrator() %}\n",
|
||
" <p><a href=\"mailto:{{ user.email }}\">{{ user.email }}</a></p>\n",
|
||
" {% endif %}\n",
|
||
" {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}\n",
|
||
" <p>\n",
|
||
" Member since {{ moment(user.member_since).format('L') }}.\n",
|
||
" Last seen {{ moment(user.last_seen).fromNow() }}.\n",
|
||
" </p>\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"This template has a few interesting implementation details:\n",
|
||
"• The name and location fields are rendered inside a single <p> element. A Jinja2\n",
|
||
"conditional ensures that the <p> element is created only when at least one of the\n",
|
||
"fields is defined.\n",
|
||
"• The user location field is rendered as a link to a Google Maps query, so that\n",
|
||
"clicking on it opens a map centered on the location.\n",
|
||
"User Profile Page \n",
|
||
"| \n",
|
||
"139\n",
|
||
"\n",
|
||
"• If the logged-in user is an administrator, then the email address of the user is\n",
|
||
"shown, rendered as a mailto link. This is useful when an administrator is viewing\n",
|
||
"the profile page of another user and needs to contact the user.\n",
|
||
"• The two timestamps for the user are rendered to the page using Flask-Moment,\n",
|
||
"as shown in Chapter 3.\n",
|
||
"As most users will want easy access to their own profile page, a link to it can be added\n",
|
||
"to the navigation bar. The relevant changes to the base.html template are shown in\n",
|
||
"Example 10-6.\n",
|
||
"Example 10-6. app/templates/base.html: add link to profile page in the navigation bar\n",
|
||
"{% if current_user.is_authenticated %}\n",
|
||
"<li>\n",
|
||
" <a href=\"{{ url_for('main.user', username=current_user.username) }}\">\n",
|
||
" Profile\n",
|
||
" </a>\n",
|
||
"</li>\n",
|
||
"{% endif %}\n",
|
||
"Using a conditional for the profile page link is necessary because the navigation bar is\n",
|
||
"also rendered for unauthenticated users, in which case the profile link is skipped.\n",
|
||
"Figure 10-1 shows how the profile page looks in the browser. The new profile link in\n",
|
||
"the navigation bar is also shown.\n",
|
||
"Figure 10-1. User profile page\n",
|
||
"140 \n",
|
||
"| \n",
|
||
"Chapter 10: User Profiles\n",
|
||
"\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 10a to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code.\n",
|
||
"Profile Editor\n",
|
||
"There are two different use cases related to editing of user profiles. The most obvious\n",
|
||
"is that users need to have access to a page where they can enter information about\n",
|
||
"themselves to present in their profile pages. A less obvious but also important\n",
|
||
"requirement is to let administrators edit the profiles of other users—not only their\n",
|
||
"personal information items but also other fields in the User model to which users\n",
|
||
"have no direct access, such as the user role. Because the two profile editing require‐\n",
|
||
"ments are substantially different, two different forms will be created.\n",
|
||
"User-Level Profile Editor\n",
|
||
"The profile editing form for regular users is shown in Example 10-7.\n",
|
||
"Example 10-7. app/main/forms.py: edit profile form\n",
|
||
"class EditProfileForm(FlaskForm):\n",
|
||
" name = StringField('Real name', validators=[Length(0, 64)])\n",
|
||
" location = StringField('Location', validators=[Length(0, 64)])\n",
|
||
" about_me = TextAreaField('About me')\n",
|
||
" submit = SubmitField('Submit')\n",
|
||
"Note that as all the fields in this form are optional, the length validator allows a length\n",
|
||
"of zero as a minimum. The route definition that uses this form is shown in\n",
|
||
"Example 10-8.\n",
|
||
"Example 10-8. app/main/views.py: edit profile route\n",
|
||
"@main.route('/edit-profile', methods=['GET', 'POST'])\n",
|
||
"@login_required\n",
|
||
"def edit_profile():\n",
|
||
" form = EditProfileForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" current_user.name = form.name.data\n",
|
||
" current_user.location = form.location.data\n",
|
||
" current_user.about_me = form.about_me.data\n",
|
||
" db.session.add(current_user._get_current_object())\n",
|
||
" db.session.commit()\n",
|
||
" flash('Your profile has been updated.')\n",
|
||
" return redirect(url_for('.user', username=current_user.username))\n",
|
||
" form.name.data = current_user.name\n",
|
||
"Profile Editor \n",
|
||
"| \n",
|
||
"141\n",
|
||
"\n",
|
||
" form.location.data = current_user.location\n",
|
||
" form.about_me.data = current_user.about_me\n",
|
||
" return render_template('edit_profile.html', form=form)\n",
|
||
"As in previous forms, the data associated with each form field is available at\n",
|
||
"form.<field-name>.data. This is useful not only to obtain values submitted by the\n",
|
||
"user, but also to provide initial values that are shown to the user for editing. When\n",
|
||
"form.validate_on_submit() is False, the three fields in this form are initialized\n",
|
||
"from the corresponding fields in current_user. Then, when the form is submitted,\n",
|
||
"the data attributes of the form fields contain the updated values, so these are moved\n",
|
||
"back into the fields of the user object before the object is saved back to the database.\n",
|
||
"Figure 10-2 shows the profile editing page.\n",
|
||
"Figure 10-2. Profile editor\n",
|
||
"To make it easy for users to reach this page, a direct link can be added in the profile\n",
|
||
"page, as shown in Example 10-9.\n",
|
||
"142 \n",
|
||
"| \n",
|
||
"Chapter 10: User Profiles\n",
|
||
"\n",
|
||
"Example 10-9. app/templates/user.html: edit profile link\n",
|
||
"{% if user == current_user %}\n",
|
||
"<a class=\"btn btn-default\" href=\"{{ url_for('.edit_profile') }}\">\n",
|
||
" Edit Profile\n",
|
||
"</a>\n",
|
||
"{% endif %}\n",
|
||
"The conditional that encloses the link will make the link appear only when users are\n",
|
||
"viewing their own profiles.\n",
|
||
"Administrator-Level Profile Editor\n",
|
||
"The profile editing form for administrators is more complex than the one for regular\n",
|
||
"users. In addition to the three profile information fields, this form allows administra‐\n",
|
||
"tors to edit a user’s email, username, confirmed status, and role. The form is shown in\n",
|
||
"Example 10-10.\n",
|
||
"Example 10-10. app/main/forms.py: profile editing form for administrators\n",
|
||
"class EditProfileAdminForm(FlaskForm):\n",
|
||
" email = StringField('Email', validators=[DataRequired(), Length(1, 64),\n",
|
||
" Email()])\n",
|
||
" username = StringField('Username', validators=[\n",
|
||
" DataRequired(), Length(1, 64),\n",
|
||
" Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,\n",
|
||
" 'Usernames must have only letters, numbers, dots or '\n",
|
||
" 'underscores')])\n",
|
||
" confirmed = BooleanField('Confirmed')\n",
|
||
" role = SelectField('Role', coerce=int)\n",
|
||
" name = StringField('Real name', validators=[Length(0, 64)])\n",
|
||
" location = StringField('Location', validators=[Length(0, 64)])\n",
|
||
" about_me = TextAreaField('About me')\n",
|
||
" submit = SubmitField('Submit')\n",
|
||
" def __init__(self, user, *args, **kwargs):\n",
|
||
" super(EditProfileAdminForm, self).__init__(*args, **kwargs)\n",
|
||
" self.role.choices = [(role.id, role.name)\n",
|
||
" for role in Role.query.order_by(Role.name).all()]\n",
|
||
" self.user = user\n",
|
||
" def validate_email(self, field):\n",
|
||
" if field.data != self.user.email and \\\n",
|
||
" User.query.filter_by(email=field.data).first():\n",
|
||
" raise ValidationError('Email already registered.')\n",
|
||
" def validate_username(self, field):\n",
|
||
" if field.data != self.user.username and \\\n",
|
||
"Profile Editor \n",
|
||
"| \n",
|
||
"143\n",
|
||
"\n",
|
||
" User.query.filter_by(username=field.data).first():\n",
|
||
" raise ValidationError('Username already in use.')\n",
|
||
"The SelectField is WTForm’s wrapper for the <select> HTML form control, which\n",
|
||
"implements a drop-down list, used in this form to select a user role. An instance of\n",
|
||
"SelectField must have the items set in its choices attribute. They must be given as a\n",
|
||
"list of tuples, with each tuple consisting of two values: an identifier for the item and\n",
|
||
"the text to show in the control as a string. The choices list is set in the form’s con‐\n",
|
||
"structor, with values obtained from the Role model with a query that sorts all the\n",
|
||
"roles alphabetically by name. The identifier for each tuple is set to the id of each role,\n",
|
||
"and since these are integers, a coerce=int argument is added to the SelectField\n",
|
||
"constructor so that the field values are stored as integers instead of the default, which\n",
|
||
"is strings.\n",
|
||
"The email and username fields are constructed in the same way as in the authentica‐\n",
|
||
"tion forms, but their validation requires some careful handling. The validation condi‐\n",
|
||
"tion used for both these fields must first check whether a change to the field was\n",
|
||
"made, and only when there is a change should it ensure that the new value does not\n",
|
||
"duplicate another user’s. When these fields are not changed, then validation should\n",
|
||
"pass. To implement this logic, the form’s constructor receives the user object as an\n",
|
||
"argument and saves it as a member variable, which is later used in the custom valida‐\n",
|
||
"tion methods.\n",
|
||
"The route definition for the administrator’s profile editor is shown in Example 10-11.\n",
|
||
"Example 10-11. app/main/views.py: edit profile route for administrators\n",
|
||
"from ..decorators import admin_required\n",
|
||
"@main.route('/edit-profile/<int:id>', methods=['GET', 'POST'])\n",
|
||
"@login_required\n",
|
||
"@admin_required\n",
|
||
"def edit_profile_admin(id):\n",
|
||
" user = User.query.get_or_404(id)\n",
|
||
" form = EditProfileAdminForm(user=user)\n",
|
||
" if form.validate_on_submit():\n",
|
||
" user.email = form.email.data\n",
|
||
" user.username = form.username.data\n",
|
||
" user.confirmed = form.confirmed.data\n",
|
||
" user.role = Role.query.get(form.role.data)\n",
|
||
" user.name = form.name.data\n",
|
||
" user.location = form.location.data\n",
|
||
" user.about_me = form.about_me.data\n",
|
||
" db.session.add(user)\n",
|
||
" db.session.commit()\n",
|
||
" flash('The profile has been updated.')\n",
|
||
" return redirect(url_for('.user', username=user.username))\n",
|
||
" form.email.data = user.email\n",
|
||
"144 \n",
|
||
"| \n",
|
||
"Chapter 10: User Profiles\n",
|
||
"\n",
|
||
" form.username.data = user.username\n",
|
||
" form.confirmed.data = user.confirmed\n",
|
||
" form.role.data = user.role_id\n",
|
||
" form.name.data = user.name\n",
|
||
" form.location.data = user.location\n",
|
||
" form.about_me.data = user.about_me\n",
|
||
" return render_template('edit_profile.html', form=form, user=user)\n",
|
||
"This route has largely the same structure as the simpler one for regular users, but it\n",
|
||
"includes the admin_required decorator created in Chapter 9, which will automati‐\n",
|
||
"cally return a 403 error for any users who are not administrators that try to use this\n",
|
||
"route.\n",
|
||
"The user id is given as a dynamic argument in the URL, so Flask-SQLAlchemy’s\n",
|
||
"get_or_404() convenience function can be used, knowing that if the id is invalid the\n",
|
||
"request will return a code 404 error. The SelectField used for the user role also\n",
|
||
"deserves to be studied. When setting the initial value for the field, the role_id is\n",
|
||
"assigned to field.role.data because the list of tuples set in the choices attribute\n",
|
||
"uses the numeric identifiers to reference each option. When the form is submitted,\n",
|
||
"the id is extracted from the field’s data attribute and used in a query to load the\n",
|
||
"selected role object by its id once again. The coerce=int argument used in the\n",
|
||
"SelectField declaration in the form ensures that the data attribute of this field is\n",
|
||
"always converted to an integer.\n",
|
||
"To link to this page, another button is added in the user profile page, as shown in\n",
|
||
"Example 10-12.\n",
|
||
"Example 10-12. app/templates/user.html: profile editing link for administrators\n",
|
||
"{% if current_user.is_administrator() %}\n",
|
||
"<a class=\"btn btn-danger\"\n",
|
||
" href=\"{{ url_for('.edit_profile_admin', id=user.id) }}\">\n",
|
||
" Edit Profile [Admin]\n",
|
||
"</a>\n",
|
||
"{% endif %}\n",
|
||
"This button is rendered with a different Bootstrap style to call attention to it. The\n",
|
||
"conditional that wraps it makes the button appear in profile pages only if the logged-\n",
|
||
"in user has the administrator role.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 10b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Profile Editor \n",
|
||
"| \n",
|
||
"145\n",
|
||
"\n",
|
||
"User Avatars\n",
|
||
"The look of the profile pages can be improved by showing avatar pictures of users. In\n",
|
||
"this section, you will learn how to add user avatars provided by Gravatar, the leading\n",
|
||
"avatar service. Gravatar associates avatar images with email addresses. Users create an\n",
|
||
"account at https://gravatar.com and then upload their images. The service exposes the\n",
|
||
"user’s avatar through a specially crafted URL that includes the MD5 hash of the user’s\n",
|
||
"email address, which can be calculated as follows:\n",
|
||
"(venv) $ python\n",
|
||
">>> import hashlib\n",
|
||
">>> hashlib.md5('john@example.com'.encode('utf-8')).hexdigest()\n",
|
||
"'d4c74594d841139328695756648b6bd6'\n",
|
||
"The avatar URLs are then generated by appending the MD5 hash to the https://\n",
|
||
"secure.gravatar.com/avatar/ URL. For example, you can type https://secure.grava‐\n",
|
||
"tar.com/avatar/d4c74594d841139328695756648b6bd6 in your browser’s address bar\n",
|
||
"to get the avatar image for the email address john@example.com, or a default avatar\n",
|
||
"image if that email address does not have an avatar registered. After you build the\n",
|
||
"basic avatar URL, a few query string arguments can be used to configure the charac‐\n",
|
||
"teristics of the avatar image, as described in Table 10-1.\n",
|
||
"Table 10-1. Gravatar query string arguments\n",
|
||
"Argument\n",
|
||
"name\n",
|
||
"Description\n",
|
||
"s\n",
|
||
"Image size, in pixels.\n",
|
||
"r\n",
|
||
"Image rating. Options are \"g\", \"pg\", \"r\", and \"x\".\n",
|
||
"d\n",
|
||
"The default image generator for users who have no avatars registered with the Gravatar service. Options\n",
|
||
"are \"404\" to return a 404 error, a URL that points to a default image, or one of the following image\n",
|
||
"generators: \"mm\", \"identicon\", \"monsterid\", \"wavatar\", \"retro\", or \"blank\".\n",
|
||
"fd\n",
|
||
"Force the use of default avatars.\n",
|
||
"For example, adding ?d=identicon to the avatar URL for john@example.com will gen‐\n",
|
||
"erate a different default avatar that is based on geometric designs. All these options to\n",
|
||
"generate avatar URLs can be added to the User model. The implementation is shown\n",
|
||
"in Example 10-13.\n",
|
||
"Example 10-13. app/models.py: gravatar URL generation\n",
|
||
"import hashlib\n",
|
||
"from flask import request\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" def gravatar(self, size=100, default='identicon', rating='g'):\n",
|
||
"146 \n",
|
||
"| \n",
|
||
"Chapter 10: User Profiles\n",
|
||
"\n",
|
||
" url = 'https://secure.gravatar.com/avatar'\n",
|
||
" hash = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()\n",
|
||
" return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(\n",
|
||
" url=url, hash=hash, size=size, default=default, rating=rating)\n",
|
||
"The avatar URL is generated from the base URL, the MD5 hash of the user’s email\n",
|
||
"address, and the arguments, all of which have default values. Note that one of the\n",
|
||
"requirements of the Gravatar service is that the email address from which the MD5\n",
|
||
"hash is obtained is normalized to contain only lowercase alphabetical characters, so\n",
|
||
"that conversion is also added to this method. With this implementation it is easy to\n",
|
||
"generate avatar URLs in the Python shell:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> u = User(email='john@example.com')\n",
|
||
">>> u.gravatar()\n",
|
||
"'https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=100&d=\n",
|
||
"identicon&r=g'\n",
|
||
">>> u.gravatar(size=256)\n",
|
||
"'https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=256&d=\n",
|
||
"identicon&r=g'\n",
|
||
"The gravatar() method can also be invoked from Jinja2 templates. Example 10-14\n",
|
||
"shows how a 256-pixel avatar can be added to the profile page.\n",
|
||
"Example 10-14. app/templates/user.html: adding an avatar to the profile page\n",
|
||
"...\n",
|
||
"<img class=\"img-rounded profile-thumbnail\" src=\"{{ user.gravatar(size=256) }}\">\n",
|
||
"<div class=\"profile-header\">\n",
|
||
" ...\n",
|
||
"</div>\n",
|
||
"...\n",
|
||
"The profile-thumbnail CSS class helps with the positioning of the image on the\n",
|
||
"page. The <div> element that follows the image encapsulates the profile information\n",
|
||
"and uses the profile-header CSS class to improve the formatting. You can see the\n",
|
||
"definition of the CSS class in the GitHub repository for the application.\n",
|
||
"Using a similar approach, the base template adds a small thumbnail image of the\n",
|
||
"logged-in user in the navigation bar. To better format the avatar pictures in the page,\n",
|
||
"custom CSS classes are used. You can find these in the source code repository in a\n",
|
||
"styles.css file added to the application’s static file folder and referenced from the\n",
|
||
"base.html template. Figure 10-3 shows the user profile page with avatar.\n",
|
||
"User Avatars \n",
|
||
"| \n",
|
||
"147\n",
|
||
"\n",
|
||
"Figure 10-3. User profile page with avatar\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 10c to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"The generation of avatars requires an MD5 hash to be generated, which is a CPU-\n",
|
||
"intensive operation. If a large number of avatars need to be generated for a page, then\n",
|
||
"the computational work can add up and become significant. Since the MD5 hash for\n",
|
||
"a user will remain constant for as long as the email address stays the same, it can be\n",
|
||
"cached in the User model. Example 10-15 shows the changes to the User model to\n",
|
||
"store the MD5 hashes in the database.\n",
|
||
"Example 10-15. app/models.py: gravatar URL generation with caching of MD5 hashes\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" avatar_hash = db.Column(db.String(32))\n",
|
||
" def __init__(self, **kwargs):\n",
|
||
" # ...\n",
|
||
" if self.email is not None and self.avatar_hash is None:\n",
|
||
"148 \n",
|
||
"| \n",
|
||
"Chapter 10: User Profiles\n",
|
||
"\n",
|
||
" self.avatar_hash = self.gravatar_hash()\n",
|
||
" def change_email(self, token):\n",
|
||
" # ...\n",
|
||
" self.email = new_email\n",
|
||
" self.avatar_hash = self.gravatar_hash()\n",
|
||
" db.session.add(self)\n",
|
||
" return True\n",
|
||
" def gravatar_hash(self):\n",
|
||
" return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()\n",
|
||
" def gravatar(self, size=100, default='identicon', rating='g'):\n",
|
||
" if request.is_secure:\n",
|
||
" url = 'https://secure.gravatar.com/avatar'\n",
|
||
" else:\n",
|
||
" url = 'http://www.gravatar.com/avatar'\n",
|
||
" hash = self.avatar_hash or self.gravatar_hash()\n",
|
||
" return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(\n",
|
||
" url=url, hash=hash, size=size, default=default, rating=rating)\n",
|
||
"To avoid duplicating the logic to compute the gravatar hash, a new method,\n",
|
||
"gravatar_hash(), is added that performs this task. During model initialization, the\n",
|
||
"hash is stored in the new avatar_hash model column. If the user updates the email\n",
|
||
"address, then the hash is recalculated. The gravatar() method uses the stored hash if\n",
|
||
"available, and if not, it generates a new hash as before.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 10d to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code.\n",
|
||
"In the next chapter, the blogging engine that powers this application will be created.\n",
|
||
"User Avatars \n",
|
||
"| \n",
|
||
"149\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 11\n",
|
||
"Blog Posts\n",
|
||
"This chapter is dedicated to the implementation of Flasky’s main feature, which is to\n",
|
||
"allow users to read and write blog posts. Here you will learn a few new techniques for\n",
|
||
"reuse of templates, pagination of long lists of items, and working with rich text.\n",
|
||
"Blog Post Submission and Display\n",
|
||
"To support blog posts, a new database model that represents them is necessary. This\n",
|
||
"model is shown in Example 11-1.\n",
|
||
"Example 11-1. app/models.py: Post model\n",
|
||
"class Post(db.Model):\n",
|
||
" __tablename__ = 'posts'\n",
|
||
" id = db.Column(db.Integer, primary_key=True)\n",
|
||
" body = db.Column(db.Text)\n",
|
||
" timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)\n",
|
||
" author_id = db.Column(db.Integer, db.ForeignKey('users.id'))\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" posts = db.relationship('Post', backref='author', lazy='dynamic')\n",
|
||
"A blog post is represented by a body, a timestamp, and a one-to-many relationship\n",
|
||
"from the User model. The body field is defined with type db.Text so that there is no\n",
|
||
"limitation on the length.\n",
|
||
"The form that will be shown in the main page of the application lets users write a blog\n",
|
||
"post. This form is very simple; it contains just a text area where the blog post can be\n",
|
||
"typed and a submit button. The form definition is shown in Example 11-2.\n",
|
||
"151\n",
|
||
"\n",
|
||
"Example 11-2. app/main/forms.py: blog post form\n",
|
||
"class PostForm(FlaskForm):\n",
|
||
" body = TextAreaField(\"What's on your mind?\", validators=[DataRequired()])\n",
|
||
" submit = SubmitField('Submit')\n",
|
||
"The index() view function handles the form and passes the list of old blog posts to\n",
|
||
"the template, as shown in Example 11-3.\n",
|
||
"Example 11-3. app/main/views.py: home page route with a blog post\n",
|
||
"@main.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" form = PostForm()\n",
|
||
" if current_user.can(Permission.WRITE_ARTICLES) and form.validate_on_submit():\n",
|
||
" post = Post(body=form.body.data,\n",
|
||
" author=current_user._get_current_object())\n",
|
||
" db.session.add(post)\n",
|
||
" db.session.commit()\n",
|
||
" return redirect(url_for('.index'))\n",
|
||
" posts = Post.query.order_by(Post.timestamp.desc()).all()\n",
|
||
" return render_template('index.html', form=form, posts=posts)\n",
|
||
"This view function passes the form and the complete list of blog posts to the template.\n",
|
||
"The list of posts is ordered by timestamp, in descending order. The blog post form is\n",
|
||
"handled in the usual manner, with the creation of a new Post instance when a valid\n",
|
||
"submission is received. The current user’s permission to write articles is checked\n",
|
||
"before allowing the new post.\n",
|
||
"Note how the author attribute of the new post object is set to the expression\n",
|
||
"current_user._get_current_object(). The current_user variable from Flask-\n",
|
||
"Login, like all context variables, is implemented as a thread-local proxy object. This\n",
|
||
"object behaves like a user object but is really a thin wrapper that contains the actual\n",
|
||
"user object inside. The database needs a real user object, which is obtained by calling\n",
|
||
"_get_current_object() on the proxy object.\n",
|
||
"The form is rendered below the greeting in the index.html template, followed by the\n",
|
||
"blog posts. The list of blog posts is a first attempt to create a blog post timeline, with\n",
|
||
"all the blog posts in the database listed in chronological order from newest to oldest.\n",
|
||
"The changes to the template are shown in Example 11-4.\n",
|
||
"152 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"Example 11-4. app/templates/index.html: home page template with blog posts\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% import \"bootstrap/wtf.html\" as wtf %}\n",
|
||
"...\n",
|
||
"<div>\n",
|
||
" {% if current_user.can(Permission.WRITE_ARTICLES) %}\n",
|
||
" {{ wtf.quick_form(form) }}\n",
|
||
" {% endif %}\n",
|
||
"</div>\n",
|
||
"<ul class=\"posts\">\n",
|
||
" {% for post in posts %}\n",
|
||
" <li class=\"post\">\n",
|
||
" <div class=\"profile-thumbnail\">\n",
|
||
" <a href=\"{{ url_for('.user', username=post.author.username) }}\">\n",
|
||
" <img class=\"img-rounded profile-thumbnail\"\n",
|
||
" src=\"{{ post.author.gravatar(size=40) }}\">\n",
|
||
" </a>\n",
|
||
" </div>\n",
|
||
" <div class=\"post-date\">{{ moment(post.timestamp).fromNow() }}</div>\n",
|
||
" <div class=\"post-author\">\n",
|
||
" <a href=\"{{ url_for('.user', username=post.author.username) }}\">\n",
|
||
" {{ post.author.username }}\n",
|
||
" </a>\n",
|
||
" </div>\n",
|
||
" <div class=\"post-body\">{{ post.body }}</div>\n",
|
||
" </li>\n",
|
||
" {% endfor %}\n",
|
||
"</ul>\n",
|
||
"...\n",
|
||
"Note that the User.can() method is used to skip the blog post form for users who do\n",
|
||
"not have the WRITE permission in their role. The blog post list is implemented as an\n",
|
||
"HTML unordered list, with CSS classes giving it a nicer formatting. A small avatar of\n",
|
||
"the author is rendered on the left side, and both the avatar and the author’s username\n",
|
||
"are rendered as links to the user’s profile page. The CSS styles used are stored in the\n",
|
||
"styles.css file located in the application’s static directory. You can review this file in the\n",
|
||
"GitHub repository. Figure 11-1 shows the home page with submission form and blog\n",
|
||
"post list.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11a to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code.\n",
|
||
"Blog Post Submission and Display \n",
|
||
"| \n",
|
||
"153\n",
|
||
"\n",
|
||
"Figure 11-1. Home page with blog submission form and blog post list\n",
|
||
"Blog Posts on Profile Pages\n",
|
||
"The user profile page can be improved by showing a list of blog posts authored by the\n",
|
||
"user. Example 11-5 shows the changes to the view function to obtain the post list.\n",
|
||
"Example 11-5. app/main/views.py: profile page route with blog posts\n",
|
||
"@main.route('/user/<username>')\n",
|
||
"def user(username):\n",
|
||
" user = User.query.filter_by(username=username).first()\n",
|
||
" if user is None:\n",
|
||
" abort(404)\n",
|
||
" posts = user.posts.order_by(Post.timestamp.desc()).all()\n",
|
||
" return render_template('user.html', user=user, posts=posts)\n",
|
||
"The list of blog posts for a user is obtained from the User.posts relationship. This\n",
|
||
"works like a query object, so filters such as order_by() can be used on it like in a\n",
|
||
"regular query object.\n",
|
||
"154 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"The user.html template needs to have the same <ul> HTML tree that renders a list of\n",
|
||
"blog posts in index.html, but having to maintain two identical copies of a piece of\n",
|
||
"HTML code is not ideal. For cases like this, Jinja2’s include directive is very useful.\n",
|
||
"The snippet of HTML that generates the post list can be moved to a separate file that\n",
|
||
"both index.html and user.html can include. Example 11-6 shows how this include\n",
|
||
"looks in user.html.\n",
|
||
"Example 11-6. app/templates/user.html: profile page template with blog posts\n",
|
||
"...\n",
|
||
"<h3>Posts by {{ user.username }}</h3>\n",
|
||
"{% include '_posts.html' %}\n",
|
||
"...\n",
|
||
"To complete this reorganization, the <ul> tree from index.html is moved to the new\n",
|
||
"template _posts.html, and replaced with another include directive like the one just\n",
|
||
"shown. Note that the use of an underscore prefix in the _posts.html template name is\n",
|
||
"not a requirement; this is merely a convention to distinguish full and partial tem‐\n",
|
||
"plates.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Paginating Long Blog Post Lists\n",
|
||
"As the site grows and the number of blog posts increases, it will become slow and\n",
|
||
"impractical to show the complete list of posts on the home and profile pages. Big\n",
|
||
"pages take longer to generate, download, and render in the web browser, so the qual‐\n",
|
||
"ity of the user experience decreases as the pages get larger. The solution is to paginate\n",
|
||
"the data and render it in chunks.\n",
|
||
"Creating Fake Blog Post Data\n",
|
||
"To be able to work with multiple pages of blog posts, it is necessary to have a test\n",
|
||
"database with a large volume of data. Manually adding new database entries is time\n",
|
||
"consuming and tedious; an automated solution is more appropriate. There are several\n",
|
||
"Python packages that can be used to generate fake information. A fairly complete one\n",
|
||
"is Faker, which is installed with pip:\n",
|
||
"(venv) $ pip install faker\n",
|
||
"Paginating Long Blog Post Lists \n",
|
||
"| \n",
|
||
"155\n",
|
||
"\n",
|
||
"The Faker package is not, strictly speaking, a dependency of the application, because\n",
|
||
"it is needed only during development. To separate the production dependencies from\n",
|
||
"the development dependencies, the requirements.txt file can be replaced with a\n",
|
||
"requirements subdirectory that stores different sets of dependencies. Inside this new\n",
|
||
"subdirectory, a dev.txt file can list the dependencies that are necessary for develop‐\n",
|
||
"ment and a prod.txt file can list the dependencies that are needed in production. As\n",
|
||
"there are a large number of dependencies that will be in both lists, a common.txt file is\n",
|
||
"added for those, and then the dev.txt and prod.txt lists use the -r prefix to include it.\n",
|
||
"Example 11-7 shows the dev.txt file.\n",
|
||
"Example 11-7. requirements/dev.txt: development requirements file\n",
|
||
"-r common.txt\n",
|
||
"faker==0.7.18\n",
|
||
"Example 11-8 shows a new module added to the application that contains two func‐\n",
|
||
"tions that generate fake users and posts.\n",
|
||
"Example 11-8. app/fake.py: generating fake users and blog posts\n",
|
||
"from random import randint\n",
|
||
"from sqlalchemy.exc import IntegrityError\n",
|
||
"from faker import Faker\n",
|
||
"from . import db\n",
|
||
"from .models import User, Post\n",
|
||
"def users(count=100):\n",
|
||
" fake = Faker()\n",
|
||
" i = 0\n",
|
||
" while i < count:\n",
|
||
" u = User(email=fake.email(),\n",
|
||
" username=fake.user_name(),\n",
|
||
" password='password',\n",
|
||
" confirmed=True,\n",
|
||
" name=fake.name(),\n",
|
||
" location=fake.city(),\n",
|
||
" about_me=fake.text(),\n",
|
||
" member_since=fake.past_date())\n",
|
||
" db.session.add(u)\n",
|
||
" try:\n",
|
||
" db.session.commit()\n",
|
||
" i += 1\n",
|
||
" except IntegrityError:\n",
|
||
" db.session.rollback()\n",
|
||
"def posts(count=100):\n",
|
||
" fake = Faker()\n",
|
||
" user_count = User.query.count()\n",
|
||
"156 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
" for i in range(count):\n",
|
||
" u = User.query.offset(randint(0, user_count - 1)).first()\n",
|
||
" p = Post(body=fake.text(),\n",
|
||
" timestamp=fake.past_date(),\n",
|
||
" author=u)\n",
|
||
" db.session.add(p)\n",
|
||
" db.session.commit()\n",
|
||
"The attributes of these fake objects are produced by random information generators\n",
|
||
"provided by the Faker package, which can generate real-looking names, emails, sen‐\n",
|
||
"tences, and many more attributes.\n",
|
||
"The email addresses and usernames of users must be unique, but since Faker gener‐\n",
|
||
"ates these in a completely random fashion, there is a risk of having duplicates. In the\n",
|
||
"unlikely event that a duplicate is generated, the database session commit will throw\n",
|
||
"an IntegrityError exception. The exception is handled by rolling back the session to\n",
|
||
"cancel that duplicate user. The loop will run until the requested number of unique\n",
|
||
"users are generated.\n",
|
||
"The random post generation must assign a random user to each post. For this, the\n",
|
||
"offset() query filter is used. This filter discards the number of results given as an\n",
|
||
"argument. By setting a random offset and then calling first(), a different random\n",
|
||
"user is obtained each time.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11c to check out this version of the applica‐\n",
|
||
"tion. To ensure that you have all the dependencies installed, also\n",
|
||
"run pip install -r requirements/dev.txt.\n",
|
||
"The new functions make it easy to create a large number of fake users and posts from\n",
|
||
"the Python shell:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> from app import fake\n",
|
||
">>> fake.users(100)\n",
|
||
">>> fake.posts(100)\n",
|
||
"If you run the application now, you will see a long list of random blog posts on the\n",
|
||
"home page, by many different users.\n",
|
||
"Rendering in Pages\n",
|
||
"Example 11-9 shows the changes to the home page route to support pagination.\n",
|
||
"Paginating Long Blog Post Lists \n",
|
||
"| \n",
|
||
"157\n",
|
||
"\n",
|
||
"Example 11-9. app/main/views.py: paginating the blog post list\n",
|
||
"@main.route('/', methods=['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" # ...\n",
|
||
" page = request.args.get('page', 1, type=int)\n",
|
||
" pagination = Post.query.order_by(Post.timestamp.desc()).paginate(\n",
|
||
" page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n",
|
||
" error_out=False)\n",
|
||
" posts = pagination.items\n",
|
||
" return render_template('index.html', form=form, posts=posts,\n",
|
||
" pagination=pagination)\n",
|
||
"The page number to render is obtained from the request’s query string, which is avail‐\n",
|
||
"able as request.args. When a page isn’t given, a default page of 1 (the first page) is\n",
|
||
"used. The type=int argument ensures that if the argument cannot be converted to an\n",
|
||
"integer, the default value is returned.\n",
|
||
"To load a single page of records, the final call to the all() method of the query object\n",
|
||
"is replaced with Flask-SQLAlchemy’s paginate(). The paginate() method takes the\n",
|
||
"page number as its first and only required argument. An optional per_page argument\n",
|
||
"can be given to indicate the size of each page, in number of items. If this argument is\n",
|
||
"not specified, the default is 20 items per page. Another optional argument called\n",
|
||
"error_out can be set to True (the default) to issue a code 404 error when a page out‐\n",
|
||
"side of the valid range is requested. If error_out is False, pages outside of the valid\n",
|
||
"range are returned with an empty list of items. To make the page sizes configurable,\n",
|
||
"the value of the per_page argument is read from an application-specific configuration\n",
|
||
"variable called FLASKY_POSTS_PER_PAGE that is added in config.py.\n",
|
||
"With these changes, the blog post list on the home page will show a limited number\n",
|
||
"of items. To see the second page of posts, add a ?page=2 query string to the URL in\n",
|
||
"the browser’s address bar.\n",
|
||
"Adding a Pagination Widget\n",
|
||
"The return value of paginate() is an object of class Pagination, a class defined by\n",
|
||
"Flask-SQLAlchemy. This object contains several properties that are useful to generate\n",
|
||
"page links in a template, so it is passed to the template as an argument. A summary of\n",
|
||
"the attributes of the pagination object is shown in Table 11-1.\n",
|
||
"158 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"Table 11-1. Flask-SQLAlchemy pagination object attributes\n",
|
||
"Attribute\n",
|
||
"Description\n",
|
||
"items\n",
|
||
"The records in the current page\n",
|
||
"query\n",
|
||
"The source query that was paginated\n",
|
||
"page\n",
|
||
"The current page number\n",
|
||
"prev_num\n",
|
||
"The previous page number\n",
|
||
"next_num\n",
|
||
"The next page number\n",
|
||
"has_next\n",
|
||
"True if there is a next page\n",
|
||
"has_prev\n",
|
||
"True if there is a previous page\n",
|
||
"pages\n",
|
||
"The total number of pages for the query\n",
|
||
"per_page The number of items per page\n",
|
||
"total\n",
|
||
"The total number of items returned by the query\n",
|
||
"The pagination object also has some methods, listed in Table 11-2.\n",
|
||
"Table 11-2. Flask-SQLAlchemy pagination object methods\n",
|
||
"Method\n",
|
||
"Description\n",
|
||
"iter_pages(left_edge=2, \n",
|
||
"left_current=2, \n",
|
||
"right_current=5, \n",
|
||
"right_edge=2)\n",
|
||
"An iterator that returns the sequence of page numbers to display in a pagination\n",
|
||
"widget. The list will have left_edge pages on the left side, left_current\n",
|
||
"pages to the left of the current page, right_current pages to the right of the\n",
|
||
"current page, and right_edge pages on the right side. For example, for page 50\n",
|
||
"of 100 this iterator configured with default values will return the following pages: 1,\n",
|
||
"2, None, 48, 49, 50, 51, 52, 53, 54, 55, None, 99, 100. A None value in the\n",
|
||
"sequence indicates a gap in the sequence of pages.\n",
|
||
"prev()\n",
|
||
"A pagination object for the previous page.\n",
|
||
"next()\n",
|
||
"A pagination object for the next page.\n",
|
||
"Armed with this powerful object and Bootstrap’s pagination CSS classes, it is quite\n",
|
||
"easy to build a pagination footer in the template. The implementation shown in\n",
|
||
"Example 11-10 is done as a reusable Jinja2 macro.\n",
|
||
"Example 11-10. app/templates/_macros.html: pagination template macro\n",
|
||
"{% macro pagination_widget(pagination, endpoint) %}\n",
|
||
"<ul class=\"pagination\">\n",
|
||
" <li{% if not pagination.has_prev %} class=\"disabled\"{% endif %}>\n",
|
||
" <a href=\"{% if pagination.has_prev %}{{ url_for(endpoint,\n",
|
||
" page = pagination.page - 1, **kwargs) }}{% else %}#{% endif %}\">\n",
|
||
" «\n",
|
||
" </a>\n",
|
||
" </li>\n",
|
||
" {% for p in pagination.iter_pages() %}\n",
|
||
"Paginating Long Blog Post Lists \n",
|
||
"| \n",
|
||
"159\n",
|
||
"\n",
|
||
" {% if p %}\n",
|
||
" {% if p == pagination.page %}\n",
|
||
" <li class=\"active\">\n",
|
||
" <a href=\"{{ url_for(endpoint, page = p, **kwargs) }}\">{{ p }}</a>\n",
|
||
" </li>\n",
|
||
" {% else %}\n",
|
||
" <li>\n",
|
||
" <a href=\"{{ url_for(endpoint, page = p, **kwargs) }}\">{{ p }}</a>\n",
|
||
" </li>\n",
|
||
" {% endif %}\n",
|
||
" {% else %}\n",
|
||
" <li class=\"disabled\"><a href=\"#\">…</a></li>\n",
|
||
" {% endif %}\n",
|
||
" {% endfor %}\n",
|
||
" <li{% if not pagination.has_next %} class=\"disabled\"{% endif %}>\n",
|
||
" <a href=\"{% if pagination.has_next %}{{ url_for(endpoint,\n",
|
||
" page = pagination.page + 1, **kwargs) }}{% else %}#{% endif %}\">\n",
|
||
" »\n",
|
||
" </a>\n",
|
||
" </li>\n",
|
||
"</ul>\n",
|
||
"{% endmacro %}\n",
|
||
"The macro creates a Bootstrap pagination element, which is a styled unordered list. It\n",
|
||
"defines the following page links inside it:\n",
|
||
"• A “previous page” link. This link gets the disabled CSS class if the current page\n",
|
||
"is the first page.\n",
|
||
"• Links to all pages returned by the pagination object’s iter_pages() iterator.\n",
|
||
"These pages are rendered as links with an explicit page number, given as an argu‐\n",
|
||
"ment to url_for(). The page currently displayed is highlighted using the active\n",
|
||
"CSS class. Gaps in the sequence of pages are rendered with the ellipsis character.\n",
|
||
"• A “next page” link. This link will appear disabled if the current page is the last\n",
|
||
"page.\n",
|
||
"Jinja2 macros always receive keyword arguments without having to include **kwargs\n",
|
||
"in the argument list. The pagination macro passes all the keyword arguments it\n",
|
||
"receives to the url_for() call that generates the pagination links. This approach can\n",
|
||
"be used with routes such as the profile page that have a dynamic part.\n",
|
||
"The pagination_widget macro can be added below the _posts.html template\n",
|
||
"included by index.html and user.html. Example 11-11 shows how it is used in the\n",
|
||
"application’s home page.\n",
|
||
"160 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"Example 11-11. app/templates/index.html: pagination footer for blog post lists\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% import \"bootstrap/wtf.html\" as wtf %}\n",
|
||
"{% import \"_macros.html\" as macros %}\n",
|
||
"...\n",
|
||
"{% include '_posts.html' %}\n",
|
||
"<div class=\"pagination\">\n",
|
||
" {{ macros.pagination_widget(pagination, '.index') }}\n",
|
||
"</div>\n",
|
||
"{% endif %}\n",
|
||
"Figure 11-2 shows how the pagination links appear in the page.\n",
|
||
"Figure 11-2. Blog post pagination\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11d to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Rich-Text Posts with Markdown and Flask-PageDown\n",
|
||
"Plain-text posts are sufficient for short messages and status updates, but users who\n",
|
||
"want to write longer articles will find the lack of formatting very limiting. In this sec‐\n",
|
||
"tion, the text area field where posts are entered will be upgraded to support the Mark‐\n",
|
||
"down syntax and present a rich-text preview of the post.\n",
|
||
"Rich-Text Posts with Markdown and Flask-PageDown \n",
|
||
"| \n",
|
||
"161\n",
|
||
"\n",
|
||
"The implementation of this feature requires a few new packages:\n",
|
||
"• PageDown, a client-side Markdown-to-HTML converter implemented in Java‐\n",
|
||
"Script\n",
|
||
"• Flask-PageDown, a PageDown wrapper for Flask that integrates PageDown with\n",
|
||
"Flask-WTF forms\n",
|
||
"• Markdown, a server-side Markdown-to-HTML converter implemented in\n",
|
||
"Python\n",
|
||
"• Bleach, an HTML sanitizer implemented in Python\n",
|
||
"The Python packages can all be installed with pip:\n",
|
||
"(venv) $ pip install flask-pagedown markdown bleach\n",
|
||
"Using Flask-PageDown\n",
|
||
"The Flask-PageDown extension defines a PageDownField class that has the same\n",
|
||
"interface as the TextAreaField from WTForms. Before this field can be used, the\n",
|
||
"extension needs to be initialized as shown in Example 11-12.\n",
|
||
"Example 11-12. app/__init__.py: Flask-PageDown initialization\n",
|
||
"from flask_pagedown import PageDown\n",
|
||
"# ...\n",
|
||
"pagedown = PageDown()\n",
|
||
"# ...\n",
|
||
"def create_app(config_name):\n",
|
||
" # ...\n",
|
||
" pagedown.init_app(app)\n",
|
||
" # ...\n",
|
||
"To convert the text area control in the home page to a Markdown rich-text editor, the\n",
|
||
"body field of the PostForm must be changed to a PageDownField as shown in\n",
|
||
"Example 11-13.\n",
|
||
"Example 11-13. app/main/forms.py: Markdown-enabled post form\n",
|
||
"from flask_pagedown.fields import PageDownField\n",
|
||
"class PostForm(FlaskForm):\n",
|
||
" body = PageDownField(\"What's on your mind?\", validators=[Required()])\n",
|
||
" submit = SubmitField('Submit')\n",
|
||
"The Markdown preview is generated with the help of the PageDown libraries, so\n",
|
||
"these must be added to the template. Flask-PageDown simplifies this task by provid‐\n",
|
||
"162 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"ing a template macro that includes the required files from a CDN as shown in\n",
|
||
"Example 11-14.\n",
|
||
"Example 11-14. app/templates/index.html: Flask-PageDown template declaration\n",
|
||
"{% block scripts %}\n",
|
||
"{{ super() }}\n",
|
||
"{{ pagedown.include_pagedown() }}\n",
|
||
"{% endblock %}\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11e to check out this version of the applica‐\n",
|
||
"tion. To ensure that you have all the dependencies installed also run\n",
|
||
"pip install -r requirements/dev.txt.\n",
|
||
"With these changes, Markdown-formatted text typed in the text area field will be\n",
|
||
"immediately rendered as HTML in the preview area below. Figure 11-3 shows the\n",
|
||
"blog submission form with rich text.\n",
|
||
"Figure 11-3. Rich-text blog post form\n",
|
||
"Rich-Text Posts with Markdown and Flask-PageDown \n",
|
||
"| \n",
|
||
"163\n",
|
||
"\n",
|
||
"Handling Rich Text on the Server\n",
|
||
"When the form is submitted, only the raw Markdown text is sent with the POST\n",
|
||
"request; the HTML preview that is shown on the page is discarded. Sending the gen‐\n",
|
||
"erated HTML preview with the form can be considered a security risk, as it would be\n",
|
||
"fairly easy for an attacker to construct HTML sequences that do not match the Mark‐\n",
|
||
"down source and submit them. To avoid any risks, only the Markdown source text is\n",
|
||
"submitted, and once in the server it is converted again to HTML using Markdown, a\n",
|
||
"Python Markdown-to-HTML converter. The resulting HTML is sanitized with\n",
|
||
"Bleach to ensure that only a short list of allowed HTML tags are used.\n",
|
||
"The conversion of the Markdown blog posts to HTML can be done in the _posts.html\n",
|
||
"template, but this is inefficient as posts will have to be converted every time they are\n",
|
||
"rendered to a page. To avoid this repetition, the conversion can be done once when\n",
|
||
"the blog post is created and then cached in the database. The HTML code for the ren‐\n",
|
||
"dered blog post is cached in a new field added to the Post model that the template\n",
|
||
"can access directly. The original Markdown source is also kept in the database in case\n",
|
||
"the post needs to be edited. Example 11-15 shows the changes to the Post model.\n",
|
||
"Example 11-15. app/models.py: Markdown text handling in the Post model\n",
|
||
"from markdown import markdown\n",
|
||
"import bleach\n",
|
||
"class Post(db.Model):\n",
|
||
" # ...\n",
|
||
" body_html = db.Column(db.Text)\n",
|
||
" # ...\n",
|
||
" @staticmethod\n",
|
||
" def on_changed_body(target, value, oldvalue, initiator):\n",
|
||
" allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',\n",
|
||
" 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',\n",
|
||
" 'h1', 'h2', 'h3', 'p']\n",
|
||
" target.body_html = bleach.linkify(bleach.clean(\n",
|
||
" markdown(value, output_format='html'),\n",
|
||
" tags=allowed_tags, strip=True))\n",
|
||
"db.event.listen(Post.body, 'set', Post.on_changed_body)\n",
|
||
"The on_changed_body() function is registered as a listener of SQLAlchemy’s “set”\n",
|
||
"event for body, which means that it will be automatically invoked whenever the body\n",
|
||
"field is set to a new value. The handler function renders the HTML version of the\n",
|
||
"body and stores it in body_html, effectively making the conversion of the Markdown\n",
|
||
"text to HTML fully automatic.\n",
|
||
"The actual conversion is done in three steps. First, the markdown() function does an\n",
|
||
"initial conversion to HTML. The result is passed to clean(), along with the list of\n",
|
||
"164 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"approved HTML tags. The clean() function removes any tags not on the whitelist.\n",
|
||
"The final conversion is done with linkify(), another function provided by Bleach\n",
|
||
"that converts any URLs written in plain text into proper <a> links. This last step is\n",
|
||
"necessary because automatic link generation is not officially in the Markdown specifi‐\n",
|
||
"cation, but is a very convenient feature. On the client side, PageDown supports this\n",
|
||
"feature as an optional extension, so linkify() matches that functionality on the\n",
|
||
"server.\n",
|
||
"The last change is to replace post.body with post.body_html in the template when\n",
|
||
"available, as shown in Example 11-16.\n",
|
||
"Example 11-16. app/templates/_posts.html: use the HTML version of the post bodies in\n",
|
||
"the template\n",
|
||
"...\n",
|
||
"<div class=\"post-body\">\n",
|
||
" {% if post.body_html %}\n",
|
||
" {{ post.body_html | safe }}\n",
|
||
" {% else %}\n",
|
||
" {{ post.body }}\n",
|
||
" {% endif %}\n",
|
||
"</div>\n",
|
||
"...\n",
|
||
"The | safe suffix when rendering the HTML body is there to tell Jinja2 not to escape\n",
|
||
"the HTML elements. Jinja2 escapes all template variables by default as a security\n",
|
||
"measure, but the Markdown-generated HTML was generated by the server, so it is\n",
|
||
"safe to render directly as HTML.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11f to check out this version of the applica‐\n",
|
||
"tion. This update also contains a database migration, so remember\n",
|
||
"to run flask db upgrade after you check out the code. To ensure\n",
|
||
"that you have all the dependencies installed also run pip install\n",
|
||
"-r requirements/dev.txt.\n",
|
||
"Permanent Links to Blog Posts\n",
|
||
"Users may want to share links to specific blog posts with friends on social networks.\n",
|
||
"For this purpose, each post will be assigned a page with a unique URL that references\n",
|
||
"it. The route and view function that support permanent links are shown in\n",
|
||
"Example 11-17.\n",
|
||
"Permanent Links to Blog Posts \n",
|
||
"| \n",
|
||
"165\n",
|
||
"\n",
|
||
"Example 11-17. app/main/views.py: enabling permanent links to posts\n",
|
||
"@main.route('/post/<int:id>')\n",
|
||
"def post(id):\n",
|
||
" post = Post.query.get_or_404(id)\n",
|
||
" return render_template('post.html', posts=[post])\n",
|
||
"The URLs that will be assigned to blog posts are constructed with the unique id field\n",
|
||
"assigned when the post is inserted in the database.\n",
|
||
"For some types of applications, building permanent links that use\n",
|
||
"readable URLs instead of numeric IDs may be preferred. An alter‐\n",
|
||
"native to numeric IDs is to assign each blog post a slug, which is a\n",
|
||
"unique string that is based on the title or first few words of the\n",
|
||
"post.\n",
|
||
"Note that the post.html template receives a list with a single element that is the post to\n",
|
||
"render. Sending a list is a matter of convenience, so that the _posts.html template ref‐\n",
|
||
"erenced by index.html and user.html can be used in this page as well.\n",
|
||
"The permanent links are added at the bottom of each post in the generic _posts.html\n",
|
||
"template, as shown in Example 11-18.\n",
|
||
"Example 11-18. app/templates/_posts.html: adding permanent links to posts\n",
|
||
"<ul class=\"posts\">\n",
|
||
" {% for post in posts %}\n",
|
||
" <li class=\"post\">\n",
|
||
" ...\n",
|
||
" <div class=\"post-content\">\n",
|
||
" ...\n",
|
||
" <div class=\"post-footer\">\n",
|
||
" <a href=\"{{ url_for('.post', id=post.id) }}\">\n",
|
||
" <span class=\"label label-default\">Permalink</span>\n",
|
||
" </a>\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
" </li>\n",
|
||
" {% endfor %}\n",
|
||
"</ul>\n",
|
||
"The new post.html template that renders the permanent link page is shown in\n",
|
||
"Example 11-19. It includes the example template.\n",
|
||
"166 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"Example 11-19. app/templates/post.html: permanent link template\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% block title %}Flasky - Post{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"{% include '_posts.html' %}\n",
|
||
"{% endblock %}\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11g to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Blog Post Editor\n",
|
||
"The last feature related to blog posts is a post editor that allows users to edit their own\n",
|
||
"posts. The blog post editor lives in a standalone page and is also based on Flask-\n",
|
||
"PageDown, so a text area where the Markdown text of the blog post can be edited is\n",
|
||
"followed by a rendered preview. The edit_post.html template is shown in\n",
|
||
"Example 11-20.\n",
|
||
"Example 11-20. app/templates/edit_post.html: edit blog post template\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% import \"bootstrap/wtf.html\" as wtf %}\n",
|
||
"{% block title %}Flasky - Edit Post{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>Edit Post</h1>\n",
|
||
"</div>\n",
|
||
"<div>\n",
|
||
" {{ wtf.quick_form(form) }}\n",
|
||
"</div>\n",
|
||
"{% endblock %}\n",
|
||
"{% block scripts %}\n",
|
||
"{{ super() }}\n",
|
||
"{{ pagedown.include_pagedown() }}\n",
|
||
"{% endblock %}\n",
|
||
"The route that supports the blog post editor is shown in Example 11-21.\n",
|
||
"Blog Post Editor \n",
|
||
"| \n",
|
||
"167\n",
|
||
"\n",
|
||
"Example 11-21. app/main/views.py: edit blog post route\n",
|
||
"@main.route('/edit/<int:id>', methods=['GET', 'POST'])\n",
|
||
"@login_required\n",
|
||
"def edit(id):\n",
|
||
" post = Post.query.get_or_404(id)\n",
|
||
" if current_user != post.author and \\\n",
|
||
" not current_user.can(Permission.ADMIN):\n",
|
||
" abort(403)\n",
|
||
" form = PostForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" post.body = form.body.data\n",
|
||
" db.session.add(post)\n",
|
||
" db.session.commit()\n",
|
||
" flash('The post has been updated.')\n",
|
||
" return redirect(url_for('.post', id=post.id))\n",
|
||
" form.body.data = post.body\n",
|
||
" return render_template('edit_post.html', form=form)\n",
|
||
"This view function is coded to allow only the author of a blog post to edit it, except\n",
|
||
"for administrators, who are allowed to edit posts from all users. If a user tries to edit a\n",
|
||
"post from another user, the view function responds with a 403 code. The PostForm\n",
|
||
"web form class used here is the same one used on the home page.\n",
|
||
"To complete the feature, a link to the blog post editor can be added below each blog\n",
|
||
"post, next to the permanent link, as shown in Example 11-22.\n",
|
||
"Example 11-22. app/templates/_posts.html: adding the edit blog post link\n",
|
||
"<ul class=\"posts\">\n",
|
||
" {% for post in posts %}\n",
|
||
" <li class=\"post\">\n",
|
||
" ...\n",
|
||
" <div class=\"post-content\">\n",
|
||
" ...\n",
|
||
" <div class=\"post-footer\">\n",
|
||
" ...\n",
|
||
" {% if current_user == post.author %}\n",
|
||
" <a href=\"{{ url_for('.edit', id=post.id) }}\">\n",
|
||
" <span class=\"label label-primary\">Edit</span>\n",
|
||
" </a>\n",
|
||
" {% elif current_user.is_administrator() %}\n",
|
||
" <a href=\"{{ url_for('.edit', id=post.id) }}\">\n",
|
||
" <span class=\"label label-danger\">Edit [Admin]</span>\n",
|
||
" </a>\n",
|
||
" {% endif %}\n",
|
||
" </div>\n",
|
||
" </div>\n",
|
||
" </li>\n",
|
||
" {% endfor %}\n",
|
||
"</ul>\n",
|
||
"168 \n",
|
||
"| \n",
|
||
"Chapter 11: Blog Posts\n",
|
||
"\n",
|
||
"This change adds an “Edit” link to any blog posts that are authored by the current\n",
|
||
"user. For administrators, the link is added to all posts. The administrator link is styled\n",
|
||
"differently as a visual cue that this is an administration feature. Figure 11-4 shows\n",
|
||
"how the Edit and Permalink links look in the web browser.\n",
|
||
"Figure 11-4. Edit and Permalink links in a blog post\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 11h to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Blog Post Editor \n",
|
||
"| \n",
|
||
"169\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 12\n",
|
||
"Followers\n",
|
||
"Socially aware web applications allow users to connect with other users. Different\n",
|
||
"applications call these relationships followers, friends, contacts, connections, or bud‐\n",
|
||
"dies, but the feature is the same regardless of the name, and in all cases involves keep‐\n",
|
||
"ing track of directional links between pairs of users and using these links in database\n",
|
||
"queries.\n",
|
||
"In this chapter, you will learn how to implement a follower feature for Flasky. Users\n",
|
||
"will be able to “follow” other users and choose to filter the blog post list on the home\n",
|
||
"page to include only those from the users they follow.\n",
|
||
"Database Relationships Revisited\n",
|
||
"As discussed in Chapter 5, databases establish links between records using relation‐\n",
|
||
"ships. The one-to-many relationship is the most common type of relationship, where\n",
|
||
"a record is linked with a list of related records. To implement this type of relationship,\n",
|
||
"the elements on the “many” side have a foreign key that points to the linked element\n",
|
||
"on the “one” side. The example application in its current state includes two one-to-\n",
|
||
"many relationships: one that links user roles to lists of users and another that links\n",
|
||
"users to the blog posts they authored.\n",
|
||
"Most other relationship types can be derived from the one-to-many type. The many-\n",
|
||
"to-one relationship is a one-to-many looked at from the point of view of the “many”\n",
|
||
"side. The one-to-one relationship type is a simplification of the one-to-many, where\n",
|
||
"the “many” side is constrained to have at most one element. The only relationship\n",
|
||
"type that cannot be implemented as a simple variation of the one-to-many model is\n",
|
||
"the many-to-many, which has lists of elements on both sides. This relationship is\n",
|
||
"described in detail in the following section.\n",
|
||
"171\n",
|
||
"\n",
|
||
"Many-to-Many Relationships\n",
|
||
"The one-to-many, many-to-one, and one-to-one relationships all have at least one\n",
|
||
"side with a single entity, so the links between related records are implemented with\n",
|
||
"foreign keys pointing to that one element. But how do you implement a relationship\n",
|
||
"where both sides are “many” sides?\n",
|
||
"Consider the classic example of a many-to-many relationship: a database of students\n",
|
||
"and the classes they are taking. Clearly, you can’t add a foreign key to a class in the\n",
|
||
"students table, because a student takes many classes—one foreign key is not enough.\n",
|
||
"Likewise, you cannot add a foreign key to a student in the classes table, because\n",
|
||
"classes have more than one student. Both sides need a list of foreign keys.\n",
|
||
"The solution is to add a third table to the database, called an association table. Now\n",
|
||
"the many-to-many relationship can be decomposed into two one-to-many relation‐\n",
|
||
"ships from each of the two original tables to the association table. Figure 12-1 shows\n",
|
||
"how the many-to-many relationship between students and classes is represented.\n",
|
||
"Figure 12-1. Many-to-many relationship example\n",
|
||
"The association table in this example is called registrations. Each row in this table\n",
|
||
"represents an individual registration of a student in a class.\n",
|
||
"Querying a many-to-many relationship is a two-step process. To obtain the list of\n",
|
||
"classes a student is taking, you start from the one-to-many relationship between stu‐\n",
|
||
"dents and registrations and get the list of registrations for the desired student. Then\n",
|
||
"the one-to-many relationship between classes and registrations is traversed in the\n",
|
||
"many-to-one direction to obtain all the classes associated with the registrations\n",
|
||
"retrieved for the student. Likewise, to find all the students in a class, you start from\n",
|
||
"the class and get a list of registrations, then get the students linked to those registra‐\n",
|
||
"tions.\n",
|
||
"Traversing two relationships to obtain query results sounds difficult, but for a simple\n",
|
||
"relationship like the one in the previous example, SQLAlchemy does most of the\n",
|
||
"work. Following is the code that represents the many-to-many relationship in\n",
|
||
"Figure 12-1:\n",
|
||
"172 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"registrations = db.Table('registrations',\n",
|
||
" db.Column('student_id', db.Integer, db.ForeignKey('students.id')),\n",
|
||
" db.Column('class_id', db.Integer, db.ForeignKey('classes.id'))\n",
|
||
")\n",
|
||
"class Student(db.Model):\n",
|
||
" id = db.Column(db.Integer, primary_key=True)\n",
|
||
" name = db.Column(db.String)\n",
|
||
" classes = db.relationship('Class',\n",
|
||
" secondary=registrations,\n",
|
||
" backref=db.backref('students', lazy='dynamic'),\n",
|
||
" lazy='dynamic')\n",
|
||
"class Class(db.Model):\n",
|
||
" id = db.Column(db.Integer, primary_key=True)\n",
|
||
" name = db.Column(db.String)\n",
|
||
"The relationship is defined with the same db.relationship() construct that is used\n",
|
||
"for one-to-many relationships, but in the case of a many-to-many relationship the\n",
|
||
"additional secondary argument must be set to the association table. The relationship\n",
|
||
"can be defined in either one of the two classes, with the backref argument taking care\n",
|
||
"of exposing the relationship from the other side as well. The association table is\n",
|
||
"defined as a simple table, not as a model, since SQLAlchemy manages this table inter‐\n",
|
||
"nally.\n",
|
||
"The classes relationship uses list semantics, which makes working with a many-to-\n",
|
||
"many relationship configured in this way extremely easy. Given a student s and a\n",
|
||
"class c, the code that registers the student for the class is:\n",
|
||
">>> s.classes.append(c)\n",
|
||
">>> db.session.add(s)\n",
|
||
"The queries that list the classes student s is registered for and the list of students reg‐\n",
|
||
"istered for class c are also very simple:\n",
|
||
">>> s.classes.all()\n",
|
||
">>> c.students.all()\n",
|
||
"The students relationship available in the Class model is the one defined in the\n",
|
||
"db.backref() argument. Note that in this relationship the backref argument was\n",
|
||
"expanded to also have a lazy='dynamic' attribute, so both sides return a query that\n",
|
||
"can accept additional filters.\n",
|
||
"If student s later decides to drop class c, you can update the database as follows:\n",
|
||
">>> s.classes.remove(c)\n",
|
||
"Database Relationships Revisited \n",
|
||
"| \n",
|
||
"173\n",
|
||
"\n",
|
||
"Self-Referential Relationships\n",
|
||
"A many-to-many relationship can be used to model users following other users, but\n",
|
||
"there is a problem. In the example of students and classes, there were two very clearly\n",
|
||
"defined entities linked together by the association table. However, to represent users\n",
|
||
"following other users, it is just users—there is no second entity.\n",
|
||
"A relationship in which both sides belong to the same table is said to be self-\n",
|
||
"referential. In this case the entities on the left side of the relationship are users, which\n",
|
||
"can be called the “followers.” The entities on the right side are also users, but these are\n",
|
||
"the “followed” users. Conceptually, self-referential relationships are no different than\n",
|
||
"regular relationships, but they are harder to think about. Figure 12-2 shows a data‐\n",
|
||
"base diagram for a self-referential relationship that represents users following other\n",
|
||
"users.\n",
|
||
"Figure 12-2. Followers, many-to-many relationship\n",
|
||
"The association table in this case is called follows. Each row in this table represents a\n",
|
||
"user following another user. The one-to-many relationship pictured on the left side\n",
|
||
"associates users with the list of “follows” rows in which they are the followers. The\n",
|
||
"one-to-many relationship pictured on the right side associates users with the list of\n",
|
||
"“follows” rows in which they are the followed user.\n",
|
||
"Advanced Many-to-Many Relationships\n",
|
||
"With a self-referential many-to-many relationship configured as shown in the previ‐\n",
|
||
"ous example, the database can represent followers—but there is one limitation. A\n",
|
||
"common need when working with many-to-many relationships is to store additional\n",
|
||
"data that applies to the link between two entities. For the followers relationship, it can\n",
|
||
"be useful to store the date a user started following another user, as that will enable\n",
|
||
"lists of followers to be presented in chronological order. The only place this informa‐\n",
|
||
"174 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"tion can be stored is in the association table, but in an implementation similar to that\n",
|
||
"of the students and classes shown earlier, the association table is an internal table that\n",
|
||
"is fully managed by SQLAlchemy.\n",
|
||
"To be able to work with custom data in the relationship, the association table must be\n",
|
||
"promoted to a proper model that the application can access. Example 12-1 shows the\n",
|
||
"new association table, represented by the Follow model.\n",
|
||
"Example 12-1. app/models.py: the follows association table as a model\n",
|
||
"class Follow(db.Model):\n",
|
||
" __tablename__ = 'follows'\n",
|
||
" follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),\n",
|
||
" primary_key=True)\n",
|
||
" followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),\n",
|
||
" primary_key=True)\n",
|
||
" timestamp = db.Column(db.DateTime, default=datetime.utcnow)\n",
|
||
"SQLAlchemy cannot use the association table transparently because that will not give\n",
|
||
"the application access to the custom fields in it. Instead, the many-to-many relation‐\n",
|
||
"ship must be decomposed into the two basic one-to-many relationships for the left\n",
|
||
"and right sides, and these must be defined as standard relationships. This is shown in\n",
|
||
"Example 12-2.\n",
|
||
"Example 12-2. app/models.py: a many-to-many relationship implemented as two one-\n",
|
||
"to-many relationships\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" followed = db.relationship('Follow',\n",
|
||
" foreign_keys=[Follow.follower_id],\n",
|
||
" backref=db.backref('follower', lazy='joined'),\n",
|
||
" lazy='dynamic',\n",
|
||
" cascade='all, delete-orphan')\n",
|
||
" followers = db.relationship('Follow',\n",
|
||
" foreign_keys=[Follow.followed_id],\n",
|
||
" backref=db.backref('followed', lazy='joined'),\n",
|
||
" lazy='dynamic',\n",
|
||
" cascade='all, delete-orphan')\n",
|
||
"Here the followed and followers relationships are defined as individual one-to-\n",
|
||
"many relationships. Note that it is necessary to eliminate any ambiguity between for‐\n",
|
||
"eign keys by specifying in each relationship which foreign key to use through the\n",
|
||
"foreign_keys optional argument. The db.backref() arguments in these relation‐\n",
|
||
"ships do not apply to each other; the back references are applied to the Follow model.\n",
|
||
"Database Relationships Revisited \n",
|
||
"| \n",
|
||
"175\n",
|
||
"\n",
|
||
"The lazy argument for the back references is specified as joined. This lazy mode\n",
|
||
"causes the related object to be loaded immediately from the join query. For example,\n",
|
||
"if a user is following 100 other users, calling user.followed.all() will return a list\n",
|
||
"of 100 Follow instances, where each one has the follower and followed back refer‐\n",
|
||
"ence properties set to the respective users. The lazy='joined' mode enables this all\n",
|
||
"to happen from a single database query. If lazy is set to the default value of select,\n",
|
||
"then the follower and followed users are loaded lazily when they are first accessed\n",
|
||
"and each attribute will require an individual query, which means that obtaining the\n",
|
||
"complete list of followed users would require 100 additional database queries.\n",
|
||
"The lazy argument on the User side of both relationships has different needs. These\n",
|
||
"are on the “one” side and return the “many” side; here a mode of dynamic is used, so\n",
|
||
"that the relationship attributes return query objects instead of returning the items\n",
|
||
"directly. This allows additional filters to be added to the query before it is executed.\n",
|
||
"The cascade argument configures how actions performed on a parent object propa‐\n",
|
||
"gate to related objects. An example of a cascade option is the rule that says that when\n",
|
||
"an object is added to the database session, any objects associated with it through rela‐\n",
|
||
"tionships should automatically be added to the session as well. The default cascade\n",
|
||
"options are appropriate for most situations, but there is one case in which the default\n",
|
||
"cascade options do not work well for this many-to-many relationship. The default\n",
|
||
"cascade behavior when an object is deleted is to set the foreign key in any related\n",
|
||
"objects that link to it to a null value. But for an association table, the correct behavior\n",
|
||
"is to delete the entries that point to a record that was deleted, as this effectively\n",
|
||
"destroys the link. This is what the delete-orphan cascade option does.\n",
|
||
"The value given to cascade is a comma-separated list of cascade\n",
|
||
"options. This is somewhat confusing, but the option named all\n",
|
||
"represents all the cascade options except delete-orphan. Using the\n",
|
||
"value all, delete-orphan leaves the default cascade options\n",
|
||
"enabled and adds the delete behavior for orphans.\n",
|
||
"The application now needs to work with the two one-to-many relationships to imple‐\n",
|
||
"ment the many-to-many functionality. Since these are operations that will need to be\n",
|
||
"repeated often, it is a good idea to create helper methods in the User model for all the\n",
|
||
"possible operations. The four new methods that control this relationship are shown in\n",
|
||
"Example 12-3.\n",
|
||
"176 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"Example 12-3. app/models.py: followers helper methods\n",
|
||
"class User(db.Model):\n",
|
||
" # ...\n",
|
||
" def follow(self, user):\n",
|
||
" if not self.is_following(user):\n",
|
||
" f = Follow(follower=self, followed=user)\n",
|
||
" db.session.add(f)\n",
|
||
" def unfollow(self, user):\n",
|
||
" f = self.followed.filter_by(followed_id=user.id).first()\n",
|
||
" if f:\n",
|
||
" db.session.delete(f)\n",
|
||
" def is_following(self, user):\n",
|
||
" if user.id is None:\n",
|
||
" return False\n",
|
||
" return self.followed.filter_by(\n",
|
||
" followed_id=user.id).first() is not None\n",
|
||
" def is_followed_by(self, user):\n",
|
||
" if user.id is None:\n",
|
||
" return False\n",
|
||
" return self.followers.filter_by(\n",
|
||
" follower_id=user.id).first() is not None\n",
|
||
"The follow() method manually inserts a Follow instance in the association table that\n",
|
||
"links a follower with a followed user, giving the application the opportunity to set the\n",
|
||
"custom field. The two users who are connecting are manually assigned to the new\n",
|
||
"Follow instance in its constructor, and then the object is added to the database ses‐\n",
|
||
"sion as usual. Note that there is no need to manually set the timestamp field because\n",
|
||
"it was defined with a default value that sets the current date and time. The\n",
|
||
"unfollow() method uses the followed relationship to locate the Follow instance that\n",
|
||
"links the user to the followed user who needs to be disconnected. To destroy the link\n",
|
||
"between the two users, the Follow object is simply deleted. The is_following() and\n",
|
||
"is_followed_by() methods search the left- and right-side one-to-many relation‐\n",
|
||
"ships, respectively, for the given user and return True if the user is found. Both ensure\n",
|
||
"that the given user has been assigned an id before issuing a query, to avoid errors if a\n",
|
||
"user that has been created but not committed to the database yet is provided.\n",
|
||
"Database Relationships Revisited \n",
|
||
"| \n",
|
||
"177\n",
|
||
"\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 12a to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code.\n",
|
||
"The database part of this feature is now complete. You can find a unit test that exerci‐\n",
|
||
"ses the new database relationship in the source code repository on GitHub.\n",
|
||
"Followers on the Profile Page\n",
|
||
"The profile page of a user needs to present a “Follow” button if the user viewing it is\n",
|
||
"not a follower, or an “Unfollow” button if the user is a follower. It is also a nice addi‐\n",
|
||
"tion to show the follower and followed counts, display the lists of followers and fol‐\n",
|
||
"lowed users, and show a “Follows you” sign when appropriate. The changes to the\n",
|
||
"user profile template are shown in Example 12-4. Figure 12-3 shows how the addi‐\n",
|
||
"tions look on the profile page.\n",
|
||
"Example 12-4. app/templates/user.html: follower enhancements to the user profile\n",
|
||
"header\n",
|
||
"{% if current_user.can(Permission.FOLLOW) and user != current_user %}\n",
|
||
" {% if not current_user.is_following(user) %}\n",
|
||
" <a href=\"{{ url_for('.follow', username=user.username) }}\"\n",
|
||
" class=\"btn btn-primary\">Follow</a>\n",
|
||
" {% else %}\n",
|
||
" <a href=\"{{ url_for('.unfollow', username=user.username) }}\"\n",
|
||
" class=\"btn btn-default\">Unfollow</a>\n",
|
||
" {% endif %}\n",
|
||
"{% endif %}\n",
|
||
"<a href=\"{{ url_for('.followers', username=user.username) }}\">\n",
|
||
" Followers: <span class=\"badge\">{{ user.followers.count() }}</span>\n",
|
||
"</a>\n",
|
||
"<a href=\"{{ url_for('.followed_by', username=user.username) }}\">\n",
|
||
" Following: <span class=\"badge\">{{ user.followed.count() }}</span>\n",
|
||
"</a>\n",
|
||
"{% if current_user.is_authenticated and user != current_user and\n",
|
||
" user.is_following(current_user) %}\n",
|
||
"| <span class=\"label label-default\">Follows you</span>\n",
|
||
"{% endif %}\n",
|
||
"178 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"Figure 12-3. Followers on the profile page\n",
|
||
"There are four new endpoints defined in these template changes. The /follow/<user‐\n",
|
||
"name> route is invoked when a user clicks the “Follow” button on another user’s pro‐\n",
|
||
"file page. The implementation is shown in Example 12-5.\n",
|
||
"Example 12-5. app/main/views.py: follow route and view function\n",
|
||
"@main.route('/follow/<username>')\n",
|
||
"@login_required\n",
|
||
"@permission_required(Permission.FOLLOW)\n",
|
||
"def follow(username):\n",
|
||
" user = User.query.filter_by(username=username).first()\n",
|
||
" if user is None:\n",
|
||
" flash('Invalid user.')\n",
|
||
" return redirect(url_for('.index'))\n",
|
||
" if current_user.is_following(user):\n",
|
||
" flash('You are already following this user.')\n",
|
||
" return redirect(url_for('.user', username=username))\n",
|
||
" current_user.follow(user)\n",
|
||
" db.session.commit()\n",
|
||
" flash('You are now following %s.' % username)\n",
|
||
" return redirect(url_for('.user', username=username))\n",
|
||
"This view function loads the requested user, verifies that it is valid and that it isn’t\n",
|
||
"already followed by the logged-in user, and then calls the follow() helper function in\n",
|
||
"Followers on the Profile Page \n",
|
||
"| \n",
|
||
"179\n",
|
||
"\n",
|
||
"the User model to establish the link. The /unfollow/<username> route is implemented\n",
|
||
"in a similar way.\n",
|
||
"The /followers/<username> route is invoked when a user clicks another user’s fol‐\n",
|
||
"lower count on the profile page. The implementation is shown in Example 12-6.\n",
|
||
"Example 12-6. app/main/views.py: followers route and view function\n",
|
||
"@main.route('/followers/<username>')\n",
|
||
"def followers(username):\n",
|
||
" user = User.query.filter_by(username=username).first()\n",
|
||
" if user is None:\n",
|
||
" flash('Invalid user.')\n",
|
||
" return redirect(url_for('.index'))\n",
|
||
" page = request.args.get('page', 1, type=int)\n",
|
||
" pagination = user.followers.paginate(\n",
|
||
" page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],\n",
|
||
" error_out=False)\n",
|
||
" follows = [{'user': item.follower, 'timestamp': item.timestamp}\n",
|
||
" for item in pagination.items]\n",
|
||
" return render_template('followers.html', user=user, title=\"Followers of\",\n",
|
||
" endpoint='.followers', pagination=pagination,\n",
|
||
" follows=follows)\n",
|
||
"This function loads and validates the requested user, then paginates its followers\n",
|
||
"relationship using the same techniques learned in Chapter 11. Because the query for\n",
|
||
"followers returns Follow instances, the list is converted into another list that has user\n",
|
||
"and timestamp fields in each entry so that rendering is simpler.\n",
|
||
"The template that renders the follower list can be written generically so that it can be\n",
|
||
"used for lists of followers and followed users. The template receives the user, a title for\n",
|
||
"the page, the endpoint to use in the pagination links, the pagination object, and the\n",
|
||
"list of results.\n",
|
||
"The followed_by endpoint is almost identical. The only difference is that the list of\n",
|
||
"users is obtained from the user.followed relationship. The template arguments are\n",
|
||
"also adjusted accordingly.\n",
|
||
"The followers.html template is implemented with a two-column table that shows user‐\n",
|
||
"names and their avatars on the left and Flask-Moment timestamps on the right. You\n",
|
||
"can consult the source code repository on GitHub to study the implementation in\n",
|
||
"detail.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 12b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"180 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"Querying Followed Posts Using a Database Join\n",
|
||
"The application’s home page currently shows all the posts in the database in descend‐\n",
|
||
"ing chronological order. With the followers feature now complete, it would be a nice\n",
|
||
"addition to give users the option to view blog posts from only the users they follow.\n",
|
||
"The obvious way to load all the posts authored by followed users is to first get the list\n",
|
||
"of those users and then get the posts from each and sort them into a single list. Of\n",
|
||
"course, that approach does not scale well; the effort required to obtain this combined\n",
|
||
"list will grow as the database grows, and operations such as pagination cannot be\n",
|
||
"done efficiently. This problem is commonly known as the “N+1 problem,” because\n",
|
||
"working with the database in this way requires issuing N+1 database queries, with N\n",
|
||
"being the number of results returned by the first query. The key to obtaining the blog\n",
|
||
"posts with good performance regardless of the database size is doing it all with a sin‐\n",
|
||
"gle query.\n",
|
||
"The database operation that can do this is called a join. A join operation takes two or\n",
|
||
"more tables and finds all the combinations of rows that satisfy a given condition. The\n",
|
||
"resulting combined rows are inserted into a temporary table that is the result of the\n",
|
||
"join. The best way to explain how joins work is through an example.\n",
|
||
"Table 12-1 shows an example users table with three users.\n",
|
||
"Table 12-1. users table\n",
|
||
"id username\n",
|
||
"1\n",
|
||
"john\n",
|
||
"2\n",
|
||
"susan\n",
|
||
"3\n",
|
||
"david\n",
|
||
"Table 12-2 shows the corresponding posts table, with some blog posts.\n",
|
||
"Table 12-2. posts table\n",
|
||
"id author_id body\n",
|
||
"1\n",
|
||
"2\n",
|
||
"Blog post by susan\n",
|
||
"2\n",
|
||
"1\n",
|
||
"Blog post by john\n",
|
||
"3\n",
|
||
"3\n",
|
||
"Blog post by david\n",
|
||
"4\n",
|
||
"1\n",
|
||
"Second blog post by john\n",
|
||
"Finally, Table 12-3 shows who is following whom. In this table you can see that john is\n",
|
||
"following david, susan is following john and david, and david is not following anyone.\n",
|
||
"Querying Followed Posts Using a Database Join \n",
|
||
"| \n",
|
||
"181\n",
|
||
"\n",
|
||
"Table 12-3. follows table\n",
|
||
"follower_id\n",
|
||
"followed_id\n",
|
||
"1\n",
|
||
"3\n",
|
||
"2\n",
|
||
"1\n",
|
||
"2\n",
|
||
"3\n",
|
||
"To obtain the list of posts by users followed by the user susan, the posts and follows\n",
|
||
"tables must be combined. First the follows table is filtered to keep just the rows that\n",
|
||
"have susan as the follower, which in this example are the last two rows. Then a tem‐\n",
|
||
"porary join table is created from all the possible combinations of rows from the posts\n",
|
||
"and filtered follows tables in which the author_id of the post is the same as the\n",
|
||
"followed_id of the follow, effectively selecting any posts that appear in the list of\n",
|
||
"users susan is following. Table 12-4 shows the result of the join operation. The col‐\n",
|
||
"umns that were used to perform the join are marked with an * in this table.\n",
|
||
"Table 12-4. Joined table\n",
|
||
"id author_id*\n",
|
||
"body\n",
|
||
"follower_id\n",
|
||
"followed_id*\n",
|
||
"2\n",
|
||
"1\n",
|
||
"Blog post by john\n",
|
||
"2\n",
|
||
"1\n",
|
||
"3\n",
|
||
"3\n",
|
||
"Blog post by david\n",
|
||
"2\n",
|
||
"3\n",
|
||
"4\n",
|
||
"1\n",
|
||
"Second blog post by john\n",
|
||
"2\n",
|
||
"1\n",
|
||
"This table contains exactly the list of blog posts authored by users that susan is follow‐\n",
|
||
"ing. The Flask-SQLAlchemy query that performs the join operation as described is\n",
|
||
"fairly complex:\n",
|
||
"return db.session.query(Post).select_from(Follow).\\\n",
|
||
" filter_by(follower_id=self.id).\\\n",
|
||
" join(Post, Follow.followed_id == Post.author_id)\n",
|
||
"All the queries that you have seen so far start from the query attribute of the model\n",
|
||
"that is queried. That format does not work well for this query, because the query\n",
|
||
"needs to return posts rows, yet the first operation that needs to be done is to apply a\n",
|
||
"filter to the follows table. So, a more basic form of the query is used instead. To fully\n",
|
||
"understand this query, each part should be looked at individually:\n",
|
||
"• db.session.query(Post) specifies that this is going to be a query that returns\n",
|
||
"Post objects.\n",
|
||
"• select_from(Follow) says that the query begins with the Follow model.\n",
|
||
"• filter_by(follower_id=self.id) performs the filtering of the follows table by\n",
|
||
"the follower user.\n",
|
||
"182 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"• join(Post, Follow.followed_id == Post.author_id) joins the results of\n",
|
||
"filter_by() with the Post objects.\n",
|
||
"The query can be simplified by swapping the order of the filter and the join:\n",
|
||
"return Post.query.join(Follow, Follow.followed_id == Post.author_id)\\\n",
|
||
" .filter(Follow.follower_id == self.id)\n",
|
||
"Issuing the join operation first means the query can be started from Post.query, so\n",
|
||
"now the only two filters that need to be applied are join() and filter(). It may\n",
|
||
"seem that doing the join first and then the filtering would be more work, but in real‐\n",
|
||
"ity these two queries are equivalent. SQLAlchemy first collects all the filters and then\n",
|
||
"generates the query in the most efficient way. The native SQL instructions for these\n",
|
||
"two queries are nearly identical, something that you can confirm by printing the\n",
|
||
"query object converted to a string (i.e., print(str(query))). The final version of this\n",
|
||
"query is added to the Post model, as shown in Example 12-7.\n",
|
||
"Example 12-7. app/models.py: obtaining followed posts\n",
|
||
"class User(db.Model):\n",
|
||
" # ...\n",
|
||
" @property\n",
|
||
" def followed_posts(self):\n",
|
||
" return Post.query.join(Follow, Follow.followed_id == Post.author_id)\\\n",
|
||
" .filter(Follow.follower_id == self.id)\n",
|
||
"Note that the followed_posts() method is defined as a property so that it does not\n",
|
||
"need the (). That way, all relationships have a consistent syntax.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 12c to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Joins are extremely hard to wrap your head around; you may need to experiment\n",
|
||
"with the example code in a shell before it all sinks in.\n",
|
||
"Showing Followed Posts on the Home Page\n",
|
||
"The home page can now give users the choice to view all blog posts or just those from\n",
|
||
"followed users. Example 12-8 shows how this choice is implemented.\n",
|
||
"Showing Followed Posts on the Home Page \n",
|
||
"| \n",
|
||
"183\n",
|
||
"\n",
|
||
"Example 12-8. app/main/views.py: showing all or followed posts\n",
|
||
"@main.route('/', methods = ['GET', 'POST'])\n",
|
||
"def index():\n",
|
||
" # ...\n",
|
||
" show_followed = False\n",
|
||
" if current_user.is_authenticated:\n",
|
||
" show_followed = bool(request.cookies.get('show_followed', ''))\n",
|
||
" if show_followed:\n",
|
||
" query = current_user.followed_posts\n",
|
||
" else:\n",
|
||
" query = Post.query\n",
|
||
" pagination = query.order_by(Post.timestamp.desc()).paginate(\n",
|
||
" page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n",
|
||
" error_out=False)\n",
|
||
" posts = pagination.items\n",
|
||
" return render_template('index.html', form=form, posts=posts,\n",
|
||
" show_followed=show_followed, pagination=pagination)\n",
|
||
"The choice of showing all or followed posts is stored in a cookie called\n",
|
||
"show_followed that, when set to a nonempty string, indicates that only followed\n",
|
||
"posts should be shown. Cookies are stored in the request object as a\n",
|
||
"request.cookies dictionary. The string value of the cookie is converted to a Boolean,\n",
|
||
"and based on its value a query local variable is set to the query that obtains the com‐\n",
|
||
"plete or filtered list of blog posts. To show all the posts, the top-level query\n",
|
||
"Post.query is used, and the recently added User.followed_posts property is used\n",
|
||
"when the list should be restricted to followed users. The query stored in the query\n",
|
||
"local variable is then paginated and the results sent to the template as before.\n",
|
||
"The show_followed cookie is set in two new routes, shown in Example 12-9.\n",
|
||
"Example 12-9. app/main/views.py: selection of all or followed posts\n",
|
||
"@main.route('/all')\n",
|
||
"@login_required\n",
|
||
"def show_all():\n",
|
||
" resp = make_response(redirect(url_for('.index')))\n",
|
||
" resp.set_cookie('show_followed', '', max_age=30*24*60*60) # 30 days\n",
|
||
" return resp\n",
|
||
"@main.route('/followed')\n",
|
||
"@login_required\n",
|
||
"def show_followed():\n",
|
||
" resp = make_response(redirect(url_for('.index')))\n",
|
||
" resp.set_cookie('show_followed', '1', max_age=30*24*60*60) # 30 days\n",
|
||
" return resp\n",
|
||
"184 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"Links to these routes are added to the home page template. When they are invoked,\n",
|
||
"the show_followed cookie is set to the proper value and a redirect back to the home\n",
|
||
"page is issued.\n",
|
||
"Cookies can be set only on a response object, so these routes need to create a\n",
|
||
"response object through make_response() instead of letting Flask do this.\n",
|
||
"The set_cookie() function takes the cookie name and the value as the first two argu‐\n",
|
||
"ments. The max_age optional argument sets the number of seconds until the cookie\n",
|
||
"expires. Not including this argument makes the cookie expire when the browser win‐\n",
|
||
"dow is closed. In this case, a maximum age of 30 days is set so that the setting is\n",
|
||
"remembered even if the user does not return to the application for several days.\n",
|
||
"The changes to the template add two navigation tabs at the top of the page that invoke\n",
|
||
"the /all or /followed routes to set the correct settings in the session. You can inspect\n",
|
||
"the template changes in detail in the source code repository on GitHub. Figure 12-4\n",
|
||
"shows how the home page looks with these changes.\n",
|
||
"Figure 12-4. Followed posts on the home page\n",
|
||
"Showing Followed Posts on the Home Page \n",
|
||
"| \n",
|
||
"185\n",
|
||
"\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 12d to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"If you try the application at this point and switch to the followed list of posts, you will\n",
|
||
"notice that your own posts do not appear in the list. This is of course correct, because\n",
|
||
"users are not followers of themselves.\n",
|
||
"Even though the queries are working as designed, most users will expect to see their\n",
|
||
"own posts when they are looking at those of their friends. The easiest way to address\n",
|
||
"this issue is to register all users as their own followers at the time they are created.\n",
|
||
"This trick is shown in Example 12-10.\n",
|
||
"Example 12-10. app/models.py: making users their own followers when they are created\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" def __init__(self, **kwargs):\n",
|
||
" # ...\n",
|
||
" self.follow(self)\n",
|
||
"Unfortunately, you likely have several users in the database who are already created\n",
|
||
"and are not following themselves. If the database is small and easy to regenerate, then\n",
|
||
"it can be deleted and re-created, but if that is not an option, then adding an update\n",
|
||
"function that fixes existing users is the proper solution. This is shown in\n",
|
||
"Example 12-11.\n",
|
||
"Example 12-11. app/models.py: making users their own followers\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" @staticmethod\n",
|
||
" def add_self_follows():\n",
|
||
" for user in User.query.all():\n",
|
||
" if not user.is_following(user):\n",
|
||
" user.follow(user)\n",
|
||
" db.session.add(user)\n",
|
||
" db.session.commit()\n",
|
||
" # ...\n",
|
||
"Now the database can be updated by running the previous example function from the\n",
|
||
"shell:\n",
|
||
"(venv) $ flask shell\n",
|
||
">>> User.add_self_follows()\n",
|
||
"186 \n",
|
||
"| \n",
|
||
"Chapter 12: Followers\n",
|
||
"\n",
|
||
"Creating functions that introduce updates to the database is a common technique\n",
|
||
"used to update applications that are deployed, as running a scripted update is less\n",
|
||
"error prone than updating databases manually. In Chapter 17 you will see how this\n",
|
||
"function and others like it can be incorporated into a deployment script.\n",
|
||
"Making all users self-followers makes the application more usable, but this change\n",
|
||
"introduces a few complications. The follower and followed user counts shown in the\n",
|
||
"user profile page are now increased by one due to the self-follower links. The num‐\n",
|
||
"bers need to be decreased by one to be accurate, which is easy to do directly in the\n",
|
||
"template \n",
|
||
"by \n",
|
||
"rendering \n",
|
||
"{{ \n",
|
||
"user.followers.count() \n",
|
||
"- \n",
|
||
"1 \n",
|
||
"}} \n",
|
||
"and\n",
|
||
"{{ user.followed.count() - 1 }}. The lists of followers and followed users also\n",
|
||
"must be adjusted to not show the same user, another simple task to do in the template\n",
|
||
"with a conditional. Finally, any unit tests that check follower counts are also affected\n",
|
||
"by the self-follower links and must be adjusted to account for the self-followers.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 12e to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"In the next chapter, the user comment subsystem will be implemented—another very\n",
|
||
"important feature of socially aware applications.\n",
|
||
"Showing Followed Posts on the Home Page \n",
|
||
"| \n",
|
||
"187\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 13\n",
|
||
"User Comments\n",
|
||
"Allowing users to interact is key to the success of a social blogging platform. In this\n",
|
||
"chapter, you will learn how to implement user comments. The techniques presented\n",
|
||
"are generic enough to be directly applicable to a large number of socially enabled\n",
|
||
"applications.\n",
|
||
"Database Representation of Comments\n",
|
||
"Comments are not very different from blog posts. Both have a body, an author, and a\n",
|
||
"timestamp, and in this particular implementation both are written with Markdown\n",
|
||
"syntax. Figure 13-1 shows a diagram of the comments table and its relationships with\n",
|
||
"other tables in the database.\n",
|
||
"Figure 13-1. Database representation of blog post comments\n",
|
||
"Comments apply to specific blog posts, so a one-to-many relationship from the posts\n",
|
||
"table is defined. This relationship can be used to obtain the list of comments associ‐\n",
|
||
"ated with a particular blog post.\n",
|
||
"The comments table is also in a one-to-many relationship with the users table. This\n",
|
||
"relationship gives access to all the comments made by a user, and indirectly how\n",
|
||
"189\n",
|
||
"\n",
|
||
"many comments a user has written, a piece of information that can be interesting to\n",
|
||
"show in user profile pages. The definition of the Comment model is shown in\n",
|
||
"Example 13-1.\n",
|
||
"Example 13-1. app/models.py: comment model\n",
|
||
"class Comment(db.Model):\n",
|
||
" __tablename__ = 'comments'\n",
|
||
" id = db.Column(db.Integer, primary_key=True)\n",
|
||
" body = db.Column(db.Text)\n",
|
||
" body_html = db.Column(db.Text)\n",
|
||
" timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)\n",
|
||
" disabled = db.Column(db.Boolean)\n",
|
||
" author_id = db.Column(db.Integer, db.ForeignKey('users.id'))\n",
|
||
" post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))\n",
|
||
" @staticmethod\n",
|
||
" def on_changed_body(target, value, oldvalue, initiator):\n",
|
||
" allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',\n",
|
||
" 'strong']\n",
|
||
" target.body_html = bleach.linkify(bleach.clean(\n",
|
||
" markdown(value, output_format='html'),\n",
|
||
" tags=allowed_tags, strip=True))\n",
|
||
"db.event.listen(Comment.body, 'set', Comment.on_changed_body)\n",
|
||
"The attributes of the Comment model are almost the same as those of Post. One addi‐\n",
|
||
"tion is the disabled field, a Boolean that will be used by moderators to suppress com‐\n",
|
||
"ments that are inappropriate or offensive. Like blog posts, comments define an event\n",
|
||
"that triggers any time the body field changes, automating the rendering of the Mark‐\n",
|
||
"down text to HTML. The process is identical to what was done for blog posts in\n",
|
||
"Chapter 11, but since comments tend to be short, the list of HTML tags that are\n",
|
||
"allowed in the conversion from Markdown is more restrictive, the paragraph-related\n",
|
||
"tags have been removed, and only the character formatting tags are left.\n",
|
||
"To complete the database changes, the User and Post models must define the one-to-\n",
|
||
"many relationships with the comments table, as shown in Example 13-2.\n",
|
||
"Example 13-2. app/models.py: one-to-many relationships from users and posts to\n",
|
||
"comments\n",
|
||
"class User(db.Model):\n",
|
||
" # ...\n",
|
||
" comments = db.relationship('Comment', backref='author', lazy='dynamic')\n",
|
||
"class Post(db.Model):\n",
|
||
" # ...\n",
|
||
" comments = db.relationship('Comment', backref='post', lazy='dynamic')\n",
|
||
"190 \n",
|
||
"| \n",
|
||
"Chapter 13: User Comments\n",
|
||
"\n",
|
||
"Comment Submission and Display\n",
|
||
"In this application, comments are displayed on the individual blog post pages that\n",
|
||
"were added as permanent links in Chapter 11. A submission form is also included on\n",
|
||
"these pages. Example 13-3 shows the web form that will be used to enter comments—\n",
|
||
"an extremely simple form that only has a text field and a submit button.\n",
|
||
"Example 13-3. app/main/forms.py: comment input form\n",
|
||
"class CommentForm(FlaskForm):\n",
|
||
" body = StringField('', validators=[DataRequired()])\n",
|
||
" submit = SubmitField('Submit')\n",
|
||
"Example 13-4 shows the updated /post/<int:id> route with support for comments.\n",
|
||
"Example 13-4. app/main/views.py: blog post comments support\n",
|
||
"@main.route('/post/<int:id>', methods=['GET', 'POST'])\n",
|
||
"def post(id):\n",
|
||
" post = Post.query.get_or_404(id)\n",
|
||
" form = CommentForm()\n",
|
||
" if form.validate_on_submit():\n",
|
||
" comment = Comment(body=form.body.data,\n",
|
||
" post=post,\n",
|
||
" author=current_user._get_current_object())\n",
|
||
" db.session.add(comment)\n",
|
||
" db.session.commit()\n",
|
||
" flash('Your comment has been published.')\n",
|
||
" return redirect(url_for('.post', id=post.id, page=-1))\n",
|
||
" page = request.args.get('page', 1, type=int)\n",
|
||
" if page == -1:\n",
|
||
" page = (post.comments.count() - 1) // \\\n",
|
||
" current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1\n",
|
||
" pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(\n",
|
||
" page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],\n",
|
||
" error_out=False)\n",
|
||
" comments = pagination.items\n",
|
||
" return render_template('post.html', posts=[post], form=form,\n",
|
||
" comments=comments, pagination=pagination)\n",
|
||
"This view function instantiates the comment form and sends it to the post.html tem‐\n",
|
||
"plate for rendering. The logic that inserts a new comment when the form is submitted\n",
|
||
"is similar to the handling of blog posts. As in the Post case, the author of the com‐\n",
|
||
"ment cannot be set directly to current_user because this is a context variable proxy\n",
|
||
"object. The expression current_user._get_current_object() returns the actual\n",
|
||
"User object.\n",
|
||
"Comment Submission and Display \n",
|
||
"| \n",
|
||
"191\n",
|
||
"\n",
|
||
"The comments are sorted by their timestamp in chronological order, so new com‐\n",
|
||
"ments are always added at the bottom of the list. When a new comment is entered,\n",
|
||
"the redirect that ends the request goes back to the same URL, but the url_for()\n",
|
||
"function sets the page to -1, a special page number that is used to request the last\n",
|
||
"page of comments so that the comment just entered is seen on the page. When the\n",
|
||
"page number is obtained from the query string and found to be -1, a calculation with\n",
|
||
"the number of comments and the page size is done to obtain the actual page number\n",
|
||
"to use.\n",
|
||
"The list of comments associated with the post is obtained through the post.comments\n",
|
||
"one-to-many relationship, sorted by comment timestamp, and paginated with the\n",
|
||
"same techniques used for blog posts. The comments and the pagination object are\n",
|
||
"sent to the template for rendering. The FLASKY_COMMENTS_PER_PAGE configuration\n",
|
||
"variable is added to config.py to control the size of each page of comments.\n",
|
||
"The comment rendering is defined in a new template, _comments.html, that is similar\n",
|
||
"to _posts.html but uses a different set of CSS classes. This template is included by\n",
|
||
"_posts.html below the body of the post, followed by a call to the pagination macro.\n",
|
||
"You can review the changes to the templates in the application’s GitHub repository.\n",
|
||
"To complete this feature, blog posts shown on the home and profile pages need links\n",
|
||
"to the pages with the comments. This is shown in Example 13-5.\n",
|
||
"Example 13-5. _app/templates/_posts.html: link to blog post comments\n",
|
||
"<a href=\"{{ url_for('.post', id=post.id) }}#comments\">\n",
|
||
" <span class=\"label label-primary\">\n",
|
||
" {{ post.comments.count() }} Comments\n",
|
||
" </span>\n",
|
||
"</a>\n",
|
||
"Note how the text of the link includes the number of comments, which is easily\n",
|
||
"obtained from the one-to-many relationship between the posts and comments tables\n",
|
||
"using SQLAlchemy’s count() filter.\n",
|
||
"Also of interest is the structure of the link to the comments page, which is built as the\n",
|
||
"permanent link for the post with a #comments suffix added. This last part is called a\n",
|
||
"URL fragment and is used to indicate an initial scroll position for the page. The web\n",
|
||
"browser looks for an element with the id given and scrolls the page so that element\n",
|
||
"appears at the top of the page. This initial position is set to the “Comments” heading\n",
|
||
"in the post.html template, which is written as <h4 id=\"comments\">Comments</h4>.\n",
|
||
"Figure 13-2 shows how the comments appear on the page.\n",
|
||
"192 \n",
|
||
"| \n",
|
||
"Chapter 13: User Comments\n",
|
||
"\n",
|
||
"Figure 13-2. Blog post comments\n",
|
||
"An additional change was made to the pagination macro. The pagination links for\n",
|
||
"comments also need the #comments fragment added, so a fragment argument was\n",
|
||
"added to the macro and passed in the macro invocation from the post.html template.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 13a to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code.\n",
|
||
"Comment Moderation\n",
|
||
"In Chapter 9 a list of user roles was defined, each with a list of permissions. One of\n",
|
||
"the permissions was Permission.MODERATE, which gives users who have it in their\n",
|
||
"roles the power to moderate comments made by others.\n",
|
||
"This feature will be exposed as a link in the navigation bar that appears only to users\n",
|
||
"who are permitted to use it. This is done in the base.html template using a condi‐\n",
|
||
"tional, as shown in Example 13-6.\n",
|
||
"Comment Moderation \n",
|
||
"| \n",
|
||
"193\n",
|
||
"\n",
|
||
"Example 13-6. app/templates/base.html: Moderate Comments link in navigation bar\n",
|
||
"...\n",
|
||
"{% if current_user.can(Permission.MODERATE) %}\n",
|
||
"<li><a href=\"{{ url_for('main.moderate') }}\">Moderate Comments</a></li>\n",
|
||
"{% endif %}\n",
|
||
"...\n",
|
||
"The moderation page shows the comments for all the posts in the same list, with the\n",
|
||
"most recent comments shown first. Below each comment is a button that can toggle\n",
|
||
"the disabled attribute. The /moderate route is shown in Example 13-7.\n",
|
||
"Example 13-7. app/main/views.py: comment moderation route\n",
|
||
"@main.route('/moderate')\n",
|
||
"@login_required\n",
|
||
"@permission_required(Permission.MODERATE)\n",
|
||
"def moderate():\n",
|
||
" page = request.args.get('page', 1, type=int)\n",
|
||
" pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(\n",
|
||
" page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],\n",
|
||
" error_out=False)\n",
|
||
" comments = pagination.items\n",
|
||
" return render_template('moderate.html', comments=comments,\n",
|
||
" pagination=pagination, page=page)\n",
|
||
"This is a very simple function that reads a page of comments from the database and\n",
|
||
"passes them on to a template for rendering. Along with the comments, the template\n",
|
||
"receives the pagination object and the current page number.\n",
|
||
"The moderate.html template, shown in Example 13-8, is also simple because it relies\n",
|
||
"on the _comments.html subtemplate created earlier for the rendering of the com‐\n",
|
||
"ments.\n",
|
||
"Example 13-8. app/templates/moderate.html: comment moderation template\n",
|
||
"{% extends \"base.html\" %}\n",
|
||
"{% import \"_macros.html\" as macros %}\n",
|
||
"{% block title %}Flasky - Comment Moderation{% endblock %}\n",
|
||
"{% block page_content %}\n",
|
||
"<div class=\"page-header\">\n",
|
||
" <h1>Comment Moderation</h1>\n",
|
||
"</div>\n",
|
||
"{% set moderate = True %}\n",
|
||
"{% include '_comments.html' %}\n",
|
||
"{% if pagination %}\n",
|
||
"<div class=\"pagination\">\n",
|
||
"194 \n",
|
||
"| \n",
|
||
"Chapter 13: User Comments\n",
|
||
"\n",
|
||
" {{ macros.pagination_widget(pagination, '.moderate') }}\n",
|
||
"</div>\n",
|
||
"{% endif %}\n",
|
||
"{% endblock %}\n",
|
||
"This template defers the rendering of the comments to the _comments.html template,\n",
|
||
"but before it hands control to the subordinate template it uses Jinja2’s set directive to\n",
|
||
"define a moderate template variable set to True. This variable is used by the _com‐\n",
|
||
"ments.html template to determine whether the moderation features need to be ren‐\n",
|
||
"dered.\n",
|
||
"The portion of the _comments.html template that renders the body of each comment\n",
|
||
"needs to be modified in two ways. For regular users (when the moderate variable is\n",
|
||
"not set), any comments that are marked as disabled should be suppressed. For mod‐\n",
|
||
"erators (when moderate is set to True), the body of the comment must be rendered\n",
|
||
"regardless of the disabled state, and below the body a button should be included to\n",
|
||
"toggle the state. Example 13-9 shows these changes.\n",
|
||
"Example 13-9. app/templates/_comments.html: rendering of the comment bodies\n",
|
||
"...\n",
|
||
"<div class=\"comment-body\">\n",
|
||
" {% if comment.disabled %}\n",
|
||
" <p></p><i>This comment has been disabled by a moderator.</i></p>\n",
|
||
" {% endif %}\n",
|
||
" {% if moderate or not comment.disabled %}\n",
|
||
" {% if comment.body_html %}\n",
|
||
" {{ comment.body_html | safe }}\n",
|
||
" {% else %}\n",
|
||
" {{ comment.body }}\n",
|
||
" {% endif %}\n",
|
||
" {% endif %}\n",
|
||
"</div>\n",
|
||
"{% if moderate %}\n",
|
||
" <br>\n",
|
||
" {% if comment.disabled %}\n",
|
||
" <a class=\"btn btn-default btn-xs\" href=\"{{ url_for('.moderate_enable',\n",
|
||
" id=comment.id, page=page) }}\">Enable</a>\n",
|
||
" {% else %}\n",
|
||
" <a class=\"btn btn-danger btn-xs\" href=\"{{ url_for('.moderate_disable',\n",
|
||
" id=comment.id, page=page) }}\">Disable</a>\n",
|
||
" {% endif %}\n",
|
||
"{% endif %}\n",
|
||
"...\n",
|
||
"With these changes, users will see a short notice for disabled comments. Moderators\n",
|
||
"will see both the notice and the comment body. Moderators will also see a button to\n",
|
||
"toggle the disabled state below each comment. The button invokes one of two new\n",
|
||
"Comment Moderation \n",
|
||
"| \n",
|
||
"195\n",
|
||
"\n",
|
||
"routes, depending on which of the two possible states the comment is changing to.\n",
|
||
"Example 13-10 shows how these routes are defined.\n",
|
||
"Example 13-10. app/main/views.py: comment moderation routes\n",
|
||
"@main.route('/moderate/enable/<int:id>')\n",
|
||
"@login_required\n",
|
||
"@permission_required(Permission.MODERATE)\n",
|
||
"def moderate_enable(id):\n",
|
||
" comment = Comment.query.get_or_404(id)\n",
|
||
" comment.disabled = False\n",
|
||
" db.session.add(comment)\n",
|
||
" return redirect(url_for('.moderate',\n",
|
||
" page=request.args.get('page', 1, type=int)))\n",
|
||
"@main.route('/moderate/disable/<int:id>')\n",
|
||
"@login_required\n",
|
||
"@permission_required(Permission.MODERATE)\n",
|
||
"def moderate_disable(id):\n",
|
||
" comment = Comment.query.get_or_404(id)\n",
|
||
" comment.disabled = True\n",
|
||
" db.session.add(comment)\n",
|
||
" return redirect(url_for('.moderate',\n",
|
||
" page=request.args.get('page', 1, type=int)))\n",
|
||
"The comment enable and disable routes load the comment object, set the disabled\n",
|
||
"field to the proper value, and write it back to the database. At the end, they redirect\n",
|
||
"back to the comment moderation page (shown in Figure 13-3), and if a page argu‐\n",
|
||
"ment was given in the query string, they include it in the redirect. The buttons in the\n",
|
||
"_comments.html template are rendered with the page argument so that the redirect\n",
|
||
"brings the user back to the same page.\n",
|
||
"196 \n",
|
||
"| \n",
|
||
"Chapter 13: User Comments\n",
|
||
"\n",
|
||
"Figure 13-3. Comment moderation page\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 13b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"The topic of social features is completed with this chapter. In the next chapter, you\n",
|
||
"will learn how to expose the application functionality as an API that clients such as\n",
|
||
"smartphone apps can use.\n",
|
||
"Comment Moderation \n",
|
||
"| \n",
|
||
"197\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 14\n",
|
||
"Application Programming Interfaces\n",
|
||
"In recent years, there has been a trend in web applications to move more and more of\n",
|
||
"the business logic to the client side, producing an architecture that is known as Rich\n",
|
||
"Internet Applications (RIAs). In RIAs, the server’s main (and sometimes only) func‐\n",
|
||
"tion is to provide the client application with data retrieval and storage services. In this\n",
|
||
"model, the server becomes a web service or application programming interface (API).\n",
|
||
"There are several protocols by which RIAs can communicate with a web service.\n",
|
||
"Remote procedure call (RPC) protocols such as XML-RPC or its derivative, the Sim‐\n",
|
||
"plified Object Access Protocol (SOAP), were popular choices a few years ago. More\n",
|
||
"recently, the Representational State Transfer (REST) architecture has emerged as the\n",
|
||
"favorite for web applications due to its being built on the familiar model of the World\n",
|
||
"Wide Web.\n",
|
||
"Flask is an ideal framework to build RESTful web services, thanks to its lightweight\n",
|
||
"nature. In this chapter, you will learn how to implement a Flask-based RESTful API.\n",
|
||
"Introduction to REST\n",
|
||
"Roy Fielding’s PhD dissertation describes the REST architectural style for web serv‐\n",
|
||
"ices in terms of its six defining characteristics:\n",
|
||
"Client–server\n",
|
||
"There must be a clear separation between clients and servers.\n",
|
||
"Stateless\n",
|
||
"A client request must contain all the information that is necessary to carry it out.\n",
|
||
"The server must not store any state about the client that persists from one request\n",
|
||
"to the next.\n",
|
||
"199\n",
|
||
"\n",
|
||
"Cache\n",
|
||
"Responses from the server can be labeled as cacheable or noncacheable so that\n",
|
||
"clients (or intermediaries between clients and servers) can use a cache for optimi‐\n",
|
||
"zation purposes.\n",
|
||
"Uniform interface\n",
|
||
"The protocol by which clients access server resources must be consistent, well\n",
|
||
"defined, and standardized. This is the most complex aspect of REST, covering the\n",
|
||
"use of unique resource identifiers, resource representations, self-descriptive mes‐\n",
|
||
"sages between client and server, and hypermedia.\n",
|
||
"Layered system\n",
|
||
"Proxy servers, caches, or gateways can be inserted between clients and servers as\n",
|
||
"necessary to improve performance, reliability, and scalability.\n",
|
||
"Code-on-demand\n",
|
||
"Clients can optionally download code from the server to execute in their context.\n",
|
||
"Resources Are Everything\n",
|
||
"The concept of resources is core to the REST architectural style. In this context, a\n",
|
||
"resource is an item of interest in the domain of the application. For example, in the\n",
|
||
"blogging application, users, blog posts, and comments are all resources.\n",
|
||
"Each resource must have a unique identifier that represents it. When working with\n",
|
||
"HTTP, identifiers for resources are URLs. Continuing with the blogging example, a\n",
|
||
"blog post could be represented by the URL /api/posts/12345, where 12345 is the iden‐\n",
|
||
"tifier for a post, such as the post’s database primary key. The format or contents of the\n",
|
||
"URL do not really matter; all that matters is that each resource URL uniquely identi‐\n",
|
||
"fies a resource.\n",
|
||
"A collection of all the resources in a class also has an assigned URL. The URL for the\n",
|
||
"collection of blog posts could be /api/posts/ and the URL for the collection of all com‐\n",
|
||
"ments could be /api/comments/.\n",
|
||
"An API can also define collection URLs that represent logical subsets of all the\n",
|
||
"resources in a class. For example, the collection of all comments in blog post 12345\n",
|
||
"could be represented by the URL /api/posts/12345/comments/. It is common to define\n",
|
||
"URLs that represent collections of resources with a trailing slash, as this gives them a\n",
|
||
"“subdirectory” representation.\n",
|
||
"Be aware that Flask applies special treatment to routes that end\n",
|
||
"with a slash. If a client requests a URL without a trailing slash and\n",
|
||
"there is a matching route that has a slash at the end, then Flask will\n",
|
||
"automatically respond with a redirect to the trailing-slash URL. No\n",
|
||
"redirects are issued for the reverse case.\n",
|
||
"200 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"Request Methods\n",
|
||
"The client application sends requests to the server at the established resource URLs\n",
|
||
"and uses the request method to indicate the desired operation. To obtain the list of\n",
|
||
"available blog posts in the blogging API the client would send a GET request to http://\n",
|
||
"www.example.com/api/posts/, and to insert a new blog post it would send a POST\n",
|
||
"request to the same URL, with the contents of the blog post in the request body. To\n",
|
||
"retrieve blog post 12345 the client would send a GET request to http://www.exam‐\n",
|
||
"ple.com/api/posts/12345. Table 14-1 lists the request methods that are commonly used\n",
|
||
"in RESTful APIs, with their meanings.\n",
|
||
"Table 14-1. HTTP request methods in RESTful APIs\n",
|
||
"Request\n",
|
||
"method\n",
|
||
"Target\n",
|
||
"Description\n",
|
||
"HTTP response\n",
|
||
"status code\n",
|
||
"GET\n",
|
||
"Individual resource URL\n",
|
||
"Obtain the resource.\n",
|
||
"200\n",
|
||
"GET\n",
|
||
"Resource collection URL Obtain the collection of resources (or one page from it if the\n",
|
||
"server implements pagination).\n",
|
||
"200\n",
|
||
"POST\n",
|
||
"Resource collection URL Create a new resource and add it to the collection. The server\n",
|
||
"chooses the URL of the new resource and returns it in a\n",
|
||
"Location header in the response.\n",
|
||
"201\n",
|
||
"PUT\n",
|
||
"Individual resource URL\n",
|
||
"Modify an existing resource. Alternatively, this method can also\n",
|
||
"be used to create a new resource when the client can choose the\n",
|
||
"resource URL.\n",
|
||
"200 or 204\n",
|
||
"DELETE\n",
|
||
"Individual resource URL\n",
|
||
"Delete a resource.\n",
|
||
"200 or 204\n",
|
||
"DELETE\n",
|
||
"Resource collection URL Delete all resources in the collection.\n",
|
||
"200 or 204\n",
|
||
"The REST architecture does not require that all methods be imple‐\n",
|
||
"mented for a resource. If the client invokes a method that is not\n",
|
||
"supported for a given resource, then a response with the 405 status\n",
|
||
"code (Method Not Allowed) should be returned. Flask handles this\n",
|
||
"error automatically.\n",
|
||
"The GET, POST, PUT, and DELETE request methods are not the only ones. The HTTP\n",
|
||
"protocol relies on other methods, such as HEAD and OPTIONS, which are automatically\n",
|
||
"implemented by Flask.\n",
|
||
"Request and Response Bodies\n",
|
||
"Resources are sent back and forth between client and server in the bodies of requests\n",
|
||
"and responses, but REST does not specify the format to use to encode resources. The\n",
|
||
"Content-Type header in requests and responses is used to indicate the format in\n",
|
||
"which a resource is encoded in the body. The standard content negotiation mecha‐\n",
|
||
"Introduction to REST \n",
|
||
"| \n",
|
||
"201\n",
|
||
"\n",
|
||
"nisms in the HTTP protocol can be used between client and server to agree on a for‐\n",
|
||
"mat that both support.\n",
|
||
"The two formats commonly used with RESTful web services are JavaScript Object\n",
|
||
"Notation (JSON) and Extensible Markup Language (XML). For web-based RIAs,\n",
|
||
"JSON is attractive due to being much more concise than XML, and because of its\n",
|
||
"close ties to JavaScript, the client-side scripting language used by web browsers.\n",
|
||
"Returning to the blog example API, a blog post resource could have the following\n",
|
||
"JSON representation:\n",
|
||
"{\n",
|
||
" \"self_url\": \"http://www.example.com/api/posts/12345\",\n",
|
||
" \"title\": \"Writing RESTful APIs in Python\",\n",
|
||
" \"author_url\": \"http://www.example.com/api/users/2\",\n",
|
||
" \"body\": \"... text of the article here ...\",\n",
|
||
" \"comments_url\": \"http://www.example.com/api/posts/12345/comments\"\n",
|
||
"}\n",
|
||
"Note how the url, author_url, and comments_url fields are fully qualified resource\n",
|
||
"URLs. This is important because these URLs allow the client to discover new resour‐\n",
|
||
"ces.\n",
|
||
"In a well-designed RESTful API, the client knows a short list of top-level resource\n",
|
||
"URLs and then discovers the rest from links included in responses, similar to how\n",
|
||
"you can discover new web pages while browsing the web by clicking on links that\n",
|
||
"appear in pages that you know about.\n",
|
||
"Versioning\n",
|
||
"In a traditional server-centric web application, the server has full control of the appli‐\n",
|
||
"cation. When an application is updated, installing the new version on the server is\n",
|
||
"enough to update all users because even the parts of the application that run in the\n",
|
||
"user’s web browser are downloaded from the server.\n",
|
||
"The situation with RIAs and web services is more complicated, because often clients\n",
|
||
"are developed independently of the server—maybe even by different people. Consider\n",
|
||
"the case of an application where the RESTful web service is used by a variety of clients\n",
|
||
"including web browsers and native smartphone clients. The web browser client can\n",
|
||
"be updated on the server at any time, but the smartphone apps cannot be updated by\n",
|
||
"force; the smartphone owner needs to allow the update to happen. Even if the smart‐\n",
|
||
"phone owner is willing to update, it is not possible to orchestrate the upgrade of all\n",
|
||
"existing instances of smartphone applications to coincide exactly with the deploy‐\n",
|
||
"ment of the new server version.\n",
|
||
"For these reasons, web services need to be more tolerant than regular web applica‐\n",
|
||
"tions and be able to work with old versions of their clients. Changes to a web service\n",
|
||
"must be done with extreme care, because backward-incompatible changes can cause\n",
|
||
"202 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"existing clients to break until they are upgraded. A common practice is to give web\n",
|
||
"services a version, which is added to all URLs defined in that version of the server\n",
|
||
"application. For example, the first release of the blogging web service could expose\n",
|
||
"the collection of blog posts at /api/v1/posts/.\n",
|
||
"Including the web service version in the URL helps keep old and new features organ‐\n",
|
||
"ized so that the server can provide new features to new clients while continuing to\n",
|
||
"support old clients. An update to the blogging service could change the JSON format\n",
|
||
"of blog posts and now expose blog posts as /api/v2/posts/, while keeping the older\n",
|
||
"JSON format for clients that connect to /api/v1/posts/.\n",
|
||
"Although supporting multiple versions of the server can become a maintenance bur‐\n",
|
||
"den, there are situations in which this is the only way to allow the application to grow\n",
|
||
"without causing problems to existing deployments. Older service versions can be\n",
|
||
"deprecated and later removed, once all clients have migrated to a newer version.\n",
|
||
"RESTful Web Services with Flask\n",
|
||
"Flask makes it very easy to create RESTful web services. The familiar route() decora‐\n",
|
||
"tor along with its methods optional argument can be used to declare the routes that\n",
|
||
"handle the resource URLs exposed by the service. Working with JSON data is also\n",
|
||
"simple, as JSON data included with a request can be obtained in dictionary format by\n",
|
||
"calling request.get_json(), and a response that needs to contain JSON can be easily\n",
|
||
"generated from a Python dictionary using Flask’s jsonify() helper function.\n",
|
||
"The following sections show how Flasky can be extended with a RESTful web service\n",
|
||
"that gives clients access to blog posts and related resources.\n",
|
||
"Creating an API Blueprint\n",
|
||
"The routes associated with a RESTful API form a self-contained subset of the applica‐\n",
|
||
"tion, so putting them in their own blueprint is the best way to keep them well organ‐\n",
|
||
"ized. The general structure of the API blueprint within the application is shown in\n",
|
||
"Example 14-1.\n",
|
||
"Example 14-1. API blueprint structure\n",
|
||
"|-flasky\n",
|
||
" |-app/\n",
|
||
" |-api\n",
|
||
" |-__init__.py\n",
|
||
" |-users.py\n",
|
||
" |-posts.py\n",
|
||
" |-comments.py\n",
|
||
" |-authentication.py\n",
|
||
" |-errors.py\n",
|
||
" |-decorators.py\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"203\n",
|
||
"\n",
|
||
"Note how the package used for the API includes a version number in its name. If in\n",
|
||
"the future a backward-incompatible version of the API needs to be introduced, it can\n",
|
||
"be added as a separate package with a different version number and both APIs can be\n",
|
||
"included in the application.\n",
|
||
"The API blueprint implements each resource in a separate module. Modules to take\n",
|
||
"care of authentication and error handling and to provide custom decorators are also\n",
|
||
"included. The blueprint constructor is shown in Example 14-2.\n",
|
||
"Example 14-2. app/api/__init__.py: API blueprint creation\n",
|
||
"from flask import Blueprint\n",
|
||
"api = Blueprint('api', __name__)\n",
|
||
"from . import authentication, posts, users, comments, errors\n",
|
||
"The structure of the blueprint package constructor is similar to that of the other blue‐\n",
|
||
"prints. Importing all the components of the blueprint is necessary so that routes and\n",
|
||
"other handlers are registered. Since many of these modules need to import the api\n",
|
||
"blueprint referenced here, the imports are done at the bottom to help prevent errors\n",
|
||
"due to circular dependencies.\n",
|
||
"The registration of the API blueprint is shown in Example 14-3.\n",
|
||
"Example 14-3. app/init.py: API blueprint registration\n",
|
||
"def create_app(config_name):\n",
|
||
" # ...\n",
|
||
" from .api import api as api_blueprint\n",
|
||
" app.register_blueprint(api_blueprint, url_prefix='/api/v1')\n",
|
||
" # ...\n",
|
||
"The API blueprint is registered with a URL prefix, so that all its routes will have their\n",
|
||
"URLs prefixed with /api/v1. Adding a prefix when registering the blueprint is a good\n",
|
||
"idea because it eliminates the need to hardcode the version number in every blueprint\n",
|
||
"route.\n",
|
||
"Error Handling\n",
|
||
"A RESTful web service informs the client of the status of a request by sending the\n",
|
||
"appropriate HTTP status code in the response, plus any additional information in the\n",
|
||
"response body. The typical status codes that a client can expect to see from a web ser‐\n",
|
||
"vice are listed in Table 14-2.\n",
|
||
"204 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"Table 14-2. HTTP response status codes typically returned by APIs\n",
|
||
"HTTP\n",
|
||
"status\n",
|
||
"code\n",
|
||
"Name\n",
|
||
"Description\n",
|
||
"200\n",
|
||
"OK\n",
|
||
"The request was completed successfully.\n",
|
||
"201\n",
|
||
"Created\n",
|
||
"The request was completed successfully and a new resource was created as a result.\n",
|
||
"202\n",
|
||
"Accepted\n",
|
||
"The request was accepted for processing, but it is still in progress and will run\n",
|
||
"asynchronously.\n",
|
||
"204\n",
|
||
"No Content\n",
|
||
"The request was completed successfully and there is no data to return in the response.\n",
|
||
"400\n",
|
||
"Bad Request\n",
|
||
"The request is invalid or inconsistent.\n",
|
||
"401\n",
|
||
"Unauthorized\n",
|
||
"The request does not include authentication information or the credentials provided are\n",
|
||
"invalid.\n",
|
||
"403\n",
|
||
"Forbidden\n",
|
||
"The authentication credentials sent with the request are insufficient for the request.\n",
|
||
"404\n",
|
||
"Not Found\n",
|
||
"The resource referenced in the URL was not found.\n",
|
||
"405\n",
|
||
"Method Not Allowed The method requested is not supported for the given resource.\n",
|
||
"500\n",
|
||
"Internal Server Error\n",
|
||
"An unexpected error occurred while processing the request.\n",
|
||
"The handling of status codes 404 and 500 presents a small complication, in that these\n",
|
||
"errors are normally generated by Flask on its own, and will return an HTML\n",
|
||
"response. This can confuse an API client, which will likely expect all responses in\n",
|
||
"JSON format.\n",
|
||
"One way to generate appropriate responses for all clients is to make the error han‐\n",
|
||
"dlers adapt their responses based on the format requested by the client, a technique\n",
|
||
"called content negotiation. Example 14-4 shows an improved 404 error handler that\n",
|
||
"responds with JSON to web service clients and with HTML to others. The 500 error\n",
|
||
"handler is written in a similar way.\n",
|
||
"Example 14-4. app/api/errors.py: 404 error handler with HTTP content negotiation\n",
|
||
"@main.app_errorhandler(404)\n",
|
||
"def page_not_found(e):\n",
|
||
" if request.accept_mimetypes.accept_json and \\\n",
|
||
" not request.accept_mimetypes.accept_html:\n",
|
||
" response = jsonify({'error': 'not found'})\n",
|
||
" response.status_code = 404\n",
|
||
" return response\n",
|
||
" return render_template('404.html'), 404\n",
|
||
"This new version of the error handler checks the Accept request header, which is\n",
|
||
"decoded into request.accept_mimetypes, to determine what format the client wants\n",
|
||
"the response in. Browsers generally do not specify any restrictions on response for‐\n",
|
||
"mats, but API clients typically do. The JSON response is generated only for clients\n",
|
||
"that include JSON in their list of accepted formats, but not HTML.\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"205\n",
|
||
"\n",
|
||
"The remaining status codes are generated explicitly by the web service, so they can be\n",
|
||
"implemented as helper functions inside the blueprint in the errors.py module.\n",
|
||
"Example 14-5 shows the implementation of the 403 error; the others are similar.\n",
|
||
"Example 14-5. app/api/errors.py: API error handler for status code 403\n",
|
||
"def forbidden(message):\n",
|
||
" response = jsonify({'error': 'forbidden', 'message': message})\n",
|
||
" response.status_code = 403\n",
|
||
" return response\n",
|
||
"View functions in the API blueprint can invoke these auxiliary functions to generate\n",
|
||
"error responses when necessary.\n",
|
||
"User Authentication with Flask-HTTPAuth\n",
|
||
"Web services, like regular web applications, need to protect information and ensure\n",
|
||
"that it is not given to unauthorized parties. For this reason, RIAs must ask their users\n",
|
||
"for login credentials and pass them to the server for verification.\n",
|
||
"It was mentioned earlier that one of the characteristics of RESTful web services is that\n",
|
||
"they are stateless, which means that the server is not allowed to “remember” anything\n",
|
||
"about the client between requests. Clients need to provide all the information neces‐\n",
|
||
"sary to carry out a request in the request itself, so all requests must include user cre‐\n",
|
||
"dentials.\n",
|
||
"The current login functionality implemented with the help of Flask-Login stores data\n",
|
||
"in the user session, which Flask stores by default in a client-side cookie, so the server\n",
|
||
"does not store any user-related information; it asks the client to store it instead. It\n",
|
||
"would appear that this implementation complies with the stateless requirement of\n",
|
||
"REST, but the use of cookies in RESTful web services falls into a gray area, as it can be\n",
|
||
"cumbersome for clients that are not web browsers to implement them. For that rea‐\n",
|
||
"son, it is generally seen as a bad design choice to use cookies in APIs.\n",
|
||
"The stateless requirement of REST may seem overly strict, but it is\n",
|
||
"not arbitrary. Stateless servers can scale very easily. If servers store\n",
|
||
"information about clients, it is necessary to ensure that the same\n",
|
||
"server always gets requests from a given client, or else to use shared\n",
|
||
"storage for client data. Both are complex problems to solve that do\n",
|
||
"not exist when the server is stateless.\n",
|
||
"Because the RESTful architecture is based on the HTTP protocol, HTTP authentica‐\n",
|
||
"tion is the preferred method used to send credentials, either in its Basic or Digest fla‐\n",
|
||
"vor. With HTTP authentication, user credentials are included in an Authorization\n",
|
||
"header with all requests.\n",
|
||
"206 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"The HTTP authentication protocol is simple enough that it can be implemented\n",
|
||
"directly, but the Flask-HTTPAuth extension provides a convenient wrapper that hides\n",
|
||
"the protocol details in a decorator similar to Flask-Login’s login_required.\n",
|
||
"Flask-HTTPAuth is installed with pip:\n",
|
||
"(venv) $ pip install flask-httpauth\n",
|
||
"To initialize the extension for HTTP Basic authentication, an object of class\n",
|
||
"HTTPBasicAuth must be created. Like Flask-Login, Flask-HTTPAuth makes no\n",
|
||
"assumptions about the procedure required to verify user credentials, so this informa‐\n",
|
||
"tion is given in a callback function. Example 14-6 shows how the extension is initial‐\n",
|
||
"ized and provided with a verification callback.\n",
|
||
"Example 14-6. app/api/authentication.py: Flask-HTTPAuth initialization\n",
|
||
"from flask_httpauth import HTTPBasicAuth\n",
|
||
"auth = HTTPBasicAuth()\n",
|
||
"@auth.verify_password\n",
|
||
"def verify_password(email, password):\n",
|
||
" if email == '':\n",
|
||
" return False\n",
|
||
" user = User.query.filter_by(email = email).first()\n",
|
||
" if not user:\n",
|
||
" return False\n",
|
||
" g.current_user = user\n",
|
||
" return user.verify_password(password)\n",
|
||
"Because this type of user authentication will be used only in the API blueprint, the\n",
|
||
"Flask-HTTPAuth extension is initialized in the blueprint package, and not in the\n",
|
||
"application package like other extensions.\n",
|
||
"The email and password are verified using the existing support in the User model.\n",
|
||
"The verification callback returns True when the login is valid and False otherwise.\n",
|
||
"The Flask-HTTPAuth extension also will invoke the callback for requests that carry\n",
|
||
"no authentication, setting both arguments to the empty string. In this case, when\n",
|
||
"email is an empty string, the function immediately returns False to block the\n",
|
||
"request; for certain applications it may be acceptable to allow the anonymous user by\n",
|
||
"returning True. The authentication callback saves the authenticated user in Flask’s g\n",
|
||
"context variable so that the view function can access it later.\n",
|
||
"Because user credentials are being exchanged with every request, it\n",
|
||
"is extremely important that the API routes are exposed over secure\n",
|
||
"HTTP so that all requests and responses are encrypted in transit.\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"207\n",
|
||
"\n",
|
||
"When the authentication credentials are invalid, the server returns a 401 status code\n",
|
||
"response to the client. Flask-HTTPAuth generates a response with this status code by\n",
|
||
"default, but to ensure that the response is consistent with other errors returned by the\n",
|
||
"API, the error response can be customized as shown in Example 14-7.\n",
|
||
"Example 14-7. _app/api/authentication.py: Flask-HTTPAuth error handler\n",
|
||
"from .errors import unauthorized\n",
|
||
"@auth.error_handler\n",
|
||
"def auth_error():\n",
|
||
" return unauthorized('Invalid credentials')\n",
|
||
"To protect a route, the auth.login_required decorator is used:\n",
|
||
"@api.route('/posts/')\n",
|
||
"@auth.login_required\n",
|
||
"def get_posts():\n",
|
||
" pass\n",
|
||
"But since all the routes in the blueprint need to be protected in the same way, the\n",
|
||
"login_required decorator can be included once in a before_request handler for the\n",
|
||
"blueprint, as shown in Example 14-8.\n",
|
||
"Example 14-8. app/api/authentication.py: before_request handler with authentication\n",
|
||
"from .errors import forbidden\n",
|
||
"@api.before_request\n",
|
||
"@auth.login_required\n",
|
||
"def before_request():\n",
|
||
" if not g.current_user.is_anonymous and \\\n",
|
||
" not g.current_user.confirmed:\n",
|
||
" return forbidden('Unconfirmed account')\n",
|
||
"Now the authentication checks will be done automatically for all the routes in the\n",
|
||
"blueprint. As an additional check, the before_request handler also rejects authenti‐\n",
|
||
"cated users who have not confirmed their accounts.\n",
|
||
"Token-Based Authentication\n",
|
||
"Clients must send authentication credentials with every request. To avoid having to\n",
|
||
"constantly transfer sensitive information such as a password, a token-based authenti‐\n",
|
||
"cation solution can be used.\n",
|
||
"In token-based authentication, the client requests an access token by sending a\n",
|
||
"request that includes the login credentials as authentication. The token can then be\n",
|
||
"used in place of the login credentials to authenticate requests. For security reasons,\n",
|
||
"208 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"tokens are issued with an associated expiration. When a token expires, the client must\n",
|
||
"reauthenticate to get a new one. The risk of a token getting into the wrong hands is\n",
|
||
"limited due to its short lifespan. Example 14-9 shows the two new methods added to\n",
|
||
"the User model that support generation and verification of authentication tokens\n",
|
||
"using itsdangerous.\n",
|
||
"Example 14-9. app/models.py: token-based authentication support\n",
|
||
"class User(db.Model):\n",
|
||
" # ...\n",
|
||
" def generate_auth_token(self, expiration):\n",
|
||
" s = Serializer(current_app.config['SECRET_KEY'],\n",
|
||
" expires_in=expiration)\n",
|
||
" return s.dumps({'id': self.id}).decode('utf-8')\n",
|
||
" @staticmethod\n",
|
||
" def verify_auth_token(token):\n",
|
||
" s = Serializer(current_app.config['SECRET_KEY'])\n",
|
||
" try:\n",
|
||
" data = s.loads(token)\n",
|
||
" except:\n",
|
||
" return None\n",
|
||
" return User.query.get(data['id'])\n",
|
||
"The generate_auth_token() method returns a signed token that encodes the user’s\n",
|
||
"id field. An expiration time given in seconds is also used. The verify_auth_token()\n",
|
||
"method takes a token and, if it’s found to be valid, returns the user stored in it. This is\n",
|
||
"a static method, as the user will be known only after the token is decoded.\n",
|
||
"To authenticate requests that come with a token, the verify_password callback for\n",
|
||
"Flask-HTTPAuth must be modified to accept tokens as well as regular credentials. \n",
|
||
"The updated callback is shown in Example 14-10.\n",
|
||
"Example 14-10. app/api/authentication.py: improved authentication verification with\n",
|
||
"token support\n",
|
||
"@auth.verify_password\n",
|
||
"def verify_password(email_or_token, password):\n",
|
||
" if email_or_token == '':\n",
|
||
" return False\n",
|
||
" if password == '':\n",
|
||
" g.current_user = User.verify_auth_token(email_or_token)\n",
|
||
" g.token_used = True\n",
|
||
" return g.current_user is not None\n",
|
||
" user = User.query.filter_by(email=email_or_token).first()\n",
|
||
" if not user:\n",
|
||
" return False\n",
|
||
" g.current_user = user\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"209\n",
|
||
"\n",
|
||
" g.token_used = False\n",
|
||
" return user.verify_password(password)\n",
|
||
"In this new version, the first authentication argument can be the email address or an\n",
|
||
"authentication token. If this field is blank, an anonymous user is assumed, as before.\n",
|
||
"If the password is blank, then the email_or_token field is assumed to be a token and\n",
|
||
"validated as such. If both fields are nonempty then regular email and password\n",
|
||
"authentication is assumed. With this implementation, token-based authentication is\n",
|
||
"optional; it is up to each client to use it or not. To give view functions the ability to\n",
|
||
"distinguish between the two authentication methods a g.token_used variable is\n",
|
||
"added.\n",
|
||
"The route that returns authentication tokens to the client is also added to the API\n",
|
||
"blueprint. The implementation is shown in Example 14-11.\n",
|
||
"Example 14-11. app/api/authentication.py: authentication token generation\n",
|
||
"@api.route('/tokens/', methods=['POST'])\n",
|
||
"def get_token():\n",
|
||
" if g.current_user.is_anonymous or g.token_used:\n",
|
||
" return unauthorized('Invalid credentials')\n",
|
||
" return jsonify({'token': g.current_user.generate_auth_token(\n",
|
||
" expiration=3600), 'expiration': 3600})\n",
|
||
"Since this route is in the blueprint, the authentication mechanisms added to the\n",
|
||
"before_request handler also apply to it. To prevent clients from authenticating to\n",
|
||
"this route using a previously obtained token instead of an email address and pass‐\n",
|
||
"word, the g.token_used variable is checked, and requests authenticated with a token\n",
|
||
"are rejected. The purpose of this is to prevent users from bypassing the token expira‐\n",
|
||
"tion by requesting a new token using the old token as authentication. The function\n",
|
||
"returns a token in the JSON response with a validity period of one hour. The period is\n",
|
||
"also included in the JSON response.\n",
|
||
"Serializing Resources to and from JSON\n",
|
||
"A frequent need when writing a web service is to convert internal representations of\n",
|
||
"resources to and from JSON, which is the transport format used in HTTP requests\n",
|
||
"and responses. The process of converting an internal representation to a transport\n",
|
||
"format such as JSON is called serialization. Example 14-12 shows a new to_json()\n",
|
||
"method added to the Post class.\n",
|
||
"210 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"Example 14-12. app/models.py: converting a post to a JSON serializable dictionary\n",
|
||
"class Post(db.Model):\n",
|
||
" # ...\n",
|
||
" def to_json(self):\n",
|
||
" json_post = {\n",
|
||
" 'url': url_for('api.get_post', id=self.id),\n",
|
||
" 'body': self.body,\n",
|
||
" 'body_html': self.body_html,\n",
|
||
" 'timestamp': self.timestamp,\n",
|
||
" 'author_url': url_for('api.get_user', id=self.author_id),\n",
|
||
" 'comments_url': url_for('api.get_post_comments', id=self.id),\n",
|
||
" 'comment_count': self.comments.count()\n",
|
||
" }\n",
|
||
" return json_post\n",
|
||
"The url, author_url, and comments_url fields need to return the URLs for the\n",
|
||
"respective resources, so these are generated with url_for() calls to other routes that\n",
|
||
"will be defined in the API blueprint.\n",
|
||
"This example shows how it is possible to return “made-up” attributes in the represen‐\n",
|
||
"tation of a resource. The comment_count field returns the number of comments that\n",
|
||
"exist for the blog post. Although this is not a real attribute of the model, it is included\n",
|
||
"in the resource representation as a convenience to the client.\n",
|
||
"The to_json() method for User models can be constructed in a similar way. This\n",
|
||
"method is shown in Example 14-13.\n",
|
||
"Example 14-13. app/models.py: converting a user to a JSON serializable dictionary\n",
|
||
"class User(UserMixin, db.Model):\n",
|
||
" # ...\n",
|
||
" def to_json(self):\n",
|
||
" json_user = {\n",
|
||
" 'url': url_for('api.get_user', id=self.id),\n",
|
||
" 'username': self.username,\n",
|
||
" 'member_since': self.member_since,\n",
|
||
" 'last_seen': self.last_seen,\n",
|
||
" 'posts_url': url_for('api.get_user_posts', id=self.id),\n",
|
||
" 'followed_posts_url': url_for('api.get_user_followed_posts',\n",
|
||
" id=self.id),\n",
|
||
" 'post_count': self.posts.count()\n",
|
||
" }\n",
|
||
" return json_user\n",
|
||
"Note how in this method some of the attributes of the user, such as email and role,\n",
|
||
"are omitted from the response for privacy reasons. This example again demonstrates\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"211\n",
|
||
"\n",
|
||
"that the representation of a resource offered to clients does not need to be identical to\n",
|
||
"the internal definition of the corresponding database model.\n",
|
||
"The inverse of serialization is called deserialization. Deserializing a JSON structure\n",
|
||
"back to a model presents the challenge that some of the data coming from the client\n",
|
||
"might be invalid, wrong, or unnecessary. Example 14-14 shows the method that cre‐\n",
|
||
"ates a Post from JSON.\n",
|
||
"Example 14-14. app/models.py: creating a blog post from JSON\n",
|
||
"from app.exceptions import ValidationError\n",
|
||
"class Post(db.Model):\n",
|
||
" # ...\n",
|
||
" @staticmethod\n",
|
||
" def from_json(json_post):\n",
|
||
" body = json_post.get('body')\n",
|
||
" if body is None or body == '':\n",
|
||
" raise ValidationError('post does not have a body')\n",
|
||
" return Post(body=body)\n",
|
||
"As you can see, this implementation chooses to only use the body attribute from the\n",
|
||
"JSON dictionary. The body_html attribute is ignored since the server-side Markdown\n",
|
||
"rendering is automatically triggered by an SQLAlchemy event whenever the body\n",
|
||
"attribute is modified. The timestamp attribute does not need to be given unless the\n",
|
||
"client is allowed to back- or future-date posts, which is not a feature this application\n",
|
||
"supports. The author_url field is not used because the client has no authority to\n",
|
||
"select the author of a blog post; the only possible value for this field is that of the\n",
|
||
"authenticated user. The comments_url and comment_count attributes are automati‐\n",
|
||
"cally generated from a database relationship, so there is no useful information in\n",
|
||
"them that is needed to create a Post. Finally, the url field is ignored because in this\n",
|
||
"implementation the resource URLs are defined by the server, not the client.\n",
|
||
"Note how error checking is done. If the body field is missing or empty then a\n",
|
||
"ValidationError exception is raised. Raising an exception is in this case the appro‐\n",
|
||
"priate way to deal with the error because this method does not have enough knowl‐\n",
|
||
"edge to properly handle the error condition. The exception effectively passes the\n",
|
||
"error up to the caller, enabling higher-level code to do the error handling. The\n",
|
||
"ValidationError class is implemented as a simple subclass of Python’s ValueError.\n",
|
||
"This implementation is shown in Example 14-15.\n",
|
||
"Example 14-15. app/exceptions.py: ValidationError exception\n",
|
||
"class ValidationError(ValueError):\n",
|
||
" pass\n",
|
||
"212 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"The application now needs to handle this exception by providing the appropriate\n",
|
||
"response to the client. To avoid having to add exception-catching code in view func‐\n",
|
||
"tions, a global exception handler can be installed using Flask’s errorhandler decora‐\n",
|
||
"tor. A handler for the ValidationError exception is shown in Example 14-16.\n",
|
||
"Example 14-16. app/api/errors.py: API error handler for ValidationError exceptions\n",
|
||
"@api.errorhandler(ValidationError)\n",
|
||
"def validation_error(e):\n",
|
||
" return bad_request(e.args[0])\n",
|
||
"The errorhandler decorator is the same one that is used to register handlers for\n",
|
||
"HTTP status codes, but in this usage it takes an Exception class as an argument. The\n",
|
||
"decorated function will be invoked any time an exception of the given class is raised.\n",
|
||
"Note that the decorator is obtained from the API blueprint, so this handler will be\n",
|
||
"invoked only when the exception is raised while a route from the blueprint is being\n",
|
||
"handled. Using this technique, the code in view functions can be written very cleanly\n",
|
||
"and concisely, without the need to include error checking. For example:\n",
|
||
"@api.route('/posts/', methods=['POST'])\n",
|
||
"def new_post():\n",
|
||
" post = Post.from_json(request.json)\n",
|
||
" post.author = g.current_user\n",
|
||
" db.session.add(post)\n",
|
||
" db.session.commit()\n",
|
||
" return jsonify(post.to_json())\n",
|
||
"Implementing Resource Endpoints\n",
|
||
"What remains is to implement the routes that handle the different resources. The GET\n",
|
||
"requests are typically the easiest because they just return information and don’t need\n",
|
||
"to make any changes. Example 14-17 shows the two GET handlers for blog posts.\n",
|
||
"Example 14-17. app/api/posts.py: GET resource handlers for posts\n",
|
||
"@api.route('/posts/')\n",
|
||
"def get_posts():\n",
|
||
" posts = Post.query.all()\n",
|
||
" return jsonify({ 'posts': [post.to_json() for post in posts] })\n",
|
||
"@api.route('/posts/<int:id>')\n",
|
||
"def get_post(id):\n",
|
||
" post = Post.query.get_or_404(id)\n",
|
||
" return jsonify(post.to_json())\n",
|
||
"The first route handles the request for the collection of posts. This function uses a list\n",
|
||
"comprehension to generate the JSON version of all the posts. The second route\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"213\n",
|
||
"\n",
|
||
"returns a single blog post and responds with a code 404 error when the given id is\n",
|
||
"not found in the database.\n",
|
||
"The POST handler for blog post resources inserts a new blog post in the database. This\n",
|
||
"route is shown in Example 14-18.\n",
|
||
"Example 14-18. app/api/posts.py: POST resource handler for posts\n",
|
||
"@api.route('/posts/', methods=['POST'])\n",
|
||
"@permission_required(Permission.WRITE)\n",
|
||
"def new_post():\n",
|
||
" post = Post.from_json(request.json)\n",
|
||
" post.author = g.current_user\n",
|
||
" db.session.add(post)\n",
|
||
" db.session.commit()\n",
|
||
" return jsonify(post.to_json()), 201, \\\n",
|
||
" {'Location': url_for('api.get_post', id=post.id)}\n",
|
||
"This view function is wrapped in a permission_required decorator (shown in an\n",
|
||
"upcoming example) that ensures that the authenticated user has the permission to\n",
|
||
"write blog posts. The actual creation of the blog post is straightforward due to the\n",
|
||
"error handling support that was implemented previously. A blog post is created from\n",
|
||
"the JSON data and its author is explicitly assigned as the authenticated user. After the\n",
|
||
"model is written to the database, a 201 status code is returned and a Location header\n",
|
||
"is added with the URL of the newly created resource.\n",
|
||
"Note that as a convenience to clients, the body of the response includes the new\n",
|
||
"resource. This will save the client from having to issue a GET request for it immedi‐\n",
|
||
"ately after creating the resource.\n",
|
||
"The permission_required decorator used to prevent unauthorized users from creat‐\n",
|
||
"ing new blog posts is similar to the one used in the application but is customized for\n",
|
||
"the API blueprint. The implementation is shown in Example 14-19.\n",
|
||
"Example 14-19. app/api/decorators.py: permission_required decorator\n",
|
||
"def permission_required(permission):\n",
|
||
" def decorator(f):\n",
|
||
" @wraps(f)\n",
|
||
" def decorated_function(*args, **kwargs):\n",
|
||
" if not g.current_user.can(permission):\n",
|
||
" return forbidden('Insufficient permissions')\n",
|
||
" return f(*args, **kwargs)\n",
|
||
" return decorated_function\n",
|
||
" return decorator\n",
|
||
"214 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"The PUT handler for blog posts, used for editing existing resources, is shown in\n",
|
||
"Example 14-20.\n",
|
||
"Example 14-20. app/api/posts.py: PUT resource handler for posts\n",
|
||
"@api.route('/posts/<int:id>', methods=['PUT'])\n",
|
||
"@permission_required(Permission.WRITE)\n",
|
||
"def edit_post(id):\n",
|
||
" post = Post.query.get_or_404(id)\n",
|
||
" if g.current_user != post.author and \\\n",
|
||
" not g.current_user.can(Permission.ADMIN):\n",
|
||
" return forbidden('Insufficient permissions')\n",
|
||
" post.body = request.json.get('body', post.body)\n",
|
||
" db.session.add(post)\n",
|
||
" db.session.commit()\n",
|
||
" return jsonify(post.to_json())\n",
|
||
"The permission checks are more complex in this case. The standard check for per‐\n",
|
||
"mission to write blog posts is done with the decorator, but to allow a user to edit a\n",
|
||
"blog post the function must also ensure that the user is the author of the post or else\n",
|
||
"is an administrator. This check is added explicitly to the view function. If this check\n",
|
||
"had to be added in many view functions, building a decorator for it would be a good\n",
|
||
"way to avoid code repetition.\n",
|
||
"Since the application does not allow deletion of posts, the handler for the DELETE\n",
|
||
"request method does not need to be implemented.\n",
|
||
"The resource handlers for users and comments are implemented in a similar way.\n",
|
||
"Table 14-3 lists the set of resources implemented for this application and the HTTP\n",
|
||
"methods each supports. The complete implementation is available for you to study in\n",
|
||
"the GitHub repository for this application.\n",
|
||
"Table 14-3. Flasky API resources\n",
|
||
"Resource URL\n",
|
||
"Method\n",
|
||
"Description\n",
|
||
"/users/<int:id>\n",
|
||
"GET\n",
|
||
"Return a user.\n",
|
||
"/users/<int:id>/posts/\n",
|
||
"GET\n",
|
||
"Return all the blog posts written by a user.\n",
|
||
"/users/<int:id>/timeline/\n",
|
||
"GET\n",
|
||
"Return all the blog posts followed by a user.\n",
|
||
"/posts/\n",
|
||
"GET\n",
|
||
"Return all the blog posts.\n",
|
||
"/posts/\n",
|
||
"POST\n",
|
||
"Create a new blog post.\n",
|
||
"/posts/<int:id>\n",
|
||
"GET\n",
|
||
"Return a blog post.\n",
|
||
"/posts/<int:id>\n",
|
||
"PUT\n",
|
||
"Modify a blog post.\n",
|
||
"/posts/<int:id>/comments/\n",
|
||
"GET\n",
|
||
"Return the comments on a blog post.\n",
|
||
"/posts/<int:id>/comments/\n",
|
||
"POST\n",
|
||
"Add a comment to a blog post.\n",
|
||
"/comments/\n",
|
||
"GET\n",
|
||
"Return all the comments.\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"215\n",
|
||
"\n",
|
||
"Resource URL\n",
|
||
"Method\n",
|
||
"Description\n",
|
||
"/comments/<int:id>\n",
|
||
"GET\n",
|
||
"Return a comment.\n",
|
||
"Note that the resources that were implemented offer only a subset of the functionality\n",
|
||
"that is available through the web application. The list of supported resources could be\n",
|
||
"expanded if necessary, such as to expose followers, to enable comment moderation,\n",
|
||
"and to implement any other features that an API client might need.\n",
|
||
"Pagination of Large Resource Collections\n",
|
||
"The GET requests that return a collection of resources can be extremely expensive and\n",
|
||
"difficult to manage for very large collections. Like web applications, web services can\n",
|
||
"choose to paginate collections.\n",
|
||
"Example 14-21 shows a possible implementation of pagination for the list of blog\n",
|
||
"posts.\n",
|
||
"Example 14-21. app/api/posts.py: Post pagination\n",
|
||
"@api.route('/posts/')\n",
|
||
"def get_posts():\n",
|
||
" page = request.args.get('page', 1, type=int)\n",
|
||
" pagination = Post.query.paginate(\n",
|
||
" page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n",
|
||
" error_out=False)\n",
|
||
" posts = pagination.items\n",
|
||
" prev = None\n",
|
||
" if pagination.has_prev:\n",
|
||
" prev = url_for('api.get_posts', page=page-1)\n",
|
||
" next = None\n",
|
||
" if pagination.has_next:\n",
|
||
" next = url_for('api.get_posts', page=page+1)\n",
|
||
" return jsonify({\n",
|
||
" 'posts': [post.to_json() for post in posts],\n",
|
||
" 'prev_url': prev,\n",
|
||
" 'next_url': next,\n",
|
||
" 'count': pagination.total\n",
|
||
" })\n",
|
||
"The posts field in the JSON response contains the data items as before, but now it is\n",
|
||
"just a page and not the complete set. The prev_url and next_url items contain the\n",
|
||
"resource URLs for the previous and following pages, or None when a page in that\n",
|
||
"direction is not available. The count value is the total number of items in the collec‐\n",
|
||
"tion.\n",
|
||
"This technique can be applied to all the routes that return collections.\n",
|
||
"216 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 14a to check out this version of the applica‐\n",
|
||
"tion. To ensure that you have all the dependencies installed, also\n",
|
||
"run pip install -r requirements/dev.txt.\n",
|
||
"Testing Web Services with HTTPie\n",
|
||
"To test a web service, an HTTP client must be used. The two most used clients for\n",
|
||
"testing Python web services from the command line are cURL and HTTPie. While\n",
|
||
"both are useful tools, the latter has a much more concise and readable command line\n",
|
||
"syntax that is tailored specifically to API requests. HTTPie is installed with pip:\n",
|
||
"(venv) $ pip install httpie\n",
|
||
"Assuming the development server is running on the default http://127.0.0.1:5000\n",
|
||
"address, a GET request can be issued from another terminal window as follows:\n",
|
||
"(venv) $ http --json --auth <email>:<password> GET \\\n",
|
||
"> http://127.0.0.1:5000/api/v1/posts\n",
|
||
"HTTP/1.0 200 OK\n",
|
||
"Content-Length: 7018\n",
|
||
"Content-Type: application/json\n",
|
||
"Date: Sun, 22 Dec 2013 08:11:24 GMT\n",
|
||
"Server: Werkzeug/0.9.4 Python/2.7.3\n",
|
||
"{\n",
|
||
" \"posts\": [\n",
|
||
" ...\n",
|
||
" ],\n",
|
||
" \"prev_url\": null\n",
|
||
" \"next_url\": \"http://127.0.0.1:5000/api/v1/posts/?page=2\",\n",
|
||
" \"count\": 150\n",
|
||
"}\n",
|
||
"Note the pagination links included in the response. Since this is the first page, a previ‐\n",
|
||
"ous page is not defined, but a URL to obtain the next page and a total count were\n",
|
||
"returned.\n",
|
||
"The following command sends a POST request to add a new blog post:\n",
|
||
"(venv) $ http --auth <email>:<password> --json POST \\ \n",
|
||
"> http://127.0.0.1:5000/api/v1/posts/ \\ \n",
|
||
"> \"body=I'm adding a post from the *command line*.\"\n",
|
||
"HTTP/1.0 201 CREATED\n",
|
||
"Content-Length: 360\n",
|
||
"Content-Type: application/json\n",
|
||
"Date: Sun, 22 Dec 2013 08:30:27 GMT\n",
|
||
"Location: http://127.0.0.1:5000/api/v1/posts/111\n",
|
||
"Server: Werkzeug/0.9.4 Python/2.7.3\n",
|
||
"{\n",
|
||
"RESTful Web Services with Flask \n",
|
||
"| \n",
|
||
"217\n",
|
||
"\n",
|
||
" \"author\": \"http://127.0.0.1:5000/api/v1/users/1\",\n",
|
||
" \"body\": \"I'm adding a post from the *command line*.\",\n",
|
||
" \"body_html\": \"<p>I'm adding a post from the <em>command line</em>.</p>\",\n",
|
||
" \"comments\": \"http://127.0.0.1:5000/api/v1/posts/111/comments\",\n",
|
||
" \"comment_count\": 0,\n",
|
||
" \"timestamp\": \"Sun, 22 Dec 2013 08:30:27 GMT\",\n",
|
||
" \"url\": \"http://127.0.0.1:5000/api/v1/posts/111\"\n",
|
||
"}\n",
|
||
"To use authentication tokens instead of a username and password, a POST request\n",
|
||
"to /api/v1/tokens/ is sent first:\n",
|
||
"(venv) $ http --auth <email>:<password> --json POST \\\n",
|
||
"> http://127.0.0.1:5000/api/v1/tokens/\n",
|
||
"HTTP/1.0 200 OK\n",
|
||
"Content-Length: 162\n",
|
||
"Content-Type: application/json\n",
|
||
"Date: Sat, 04 Jan 2014 08:38:47 GMT\n",
|
||
"Server: Werkzeug/0.9.4 Python/3.3.3\n",
|
||
"{\n",
|
||
" \"expiration\": 3600,\n",
|
||
" \"token\": \"eyJpYXQiOjEzODg4MjQ3MjcsImV4cCI6MTM4ODgyODMyNywiYWxnIjoiSFMy...\"\n",
|
||
"}\n",
|
||
"And now the returned token can be used to make calls into the API for the next hour\n",
|
||
"by passing it along in the username field and leaving the password empty:\n",
|
||
"(venv) $ http --json --auth eyJpYXQ...: GET http://127.0.0.1:5000/api/v1/posts/ \n",
|
||
"When the token expires, requests will be returned with a code 401 error, indicating\n",
|
||
"that a new token needs to be obtained.\n",
|
||
"Congratulations! This chapter completes Part II, and with that the feature develop‐\n",
|
||
"ment phase of Flasky is complete. The next step is obviously to deploy it, and that\n",
|
||
"brings a new set of challenges that are the subject of Part III.\n",
|
||
"218 \n",
|
||
"| \n",
|
||
"Chapter 14: Application Programming Interfaces\n",
|
||
"\n",
|
||
"PART III\n",
|
||
"The Last Mile\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 15\n",
|
||
"Testing\n",
|
||
"There are two very good reasons for writing unit tests. When implementing new\n",
|
||
"functionality, unit tests are used to confirm that the new code is working in the\n",
|
||
"expected way. The same result can be obtained by testing manually, but of course\n",
|
||
"automated tests save time and effort because they can be repeated easily.\n",
|
||
"A second, more important reason is that each time the application is modified, all the\n",
|
||
"unit tests built around it can be executed to ensure that there are no regressions in the\n",
|
||
"existing code; in other words, that the new changes did not affect the way the older\n",
|
||
"code works.\n",
|
||
"Unit tests have been a part of Flasky since the very beginning, with tests designed to\n",
|
||
"exercise specific features of the application implemented in the database model\n",
|
||
"classes. These classes are easy to test outside of the context of a running application,\n",
|
||
"so given that it takes little effort, implementing unit tests for all the features that exist\n",
|
||
"in the database models is the best way to ensure at least that part of the application\n",
|
||
"starts robust and stays that way.\n",
|
||
"This chapter discusses ways to improve and extend unit testing to other areas of the\n",
|
||
"application.\n",
|
||
"Obtaining Code Coverage Reports\n",
|
||
"Having a test suite is important, but it is equally important to know how good or bad\n",
|
||
"it is. Code coverage tools measure how much of the application is exercised by unit\n",
|
||
"tests and can provide a detailed report that indicates which parts of the application\n",
|
||
"code are not being tested. This information is invaluable, because it can be used to\n",
|
||
"direct the effort of writing new tests to the areas that need it most.\n",
|
||
"221\n",
|
||
"\n",
|
||
"Python has an excellent code coverage tool appropriately called coverage. You can\n",
|
||
"install it with pip:\n",
|
||
"(venv) $ pip install coverage\n",
|
||
"This tool comes as a command-line script that can launch any Python application\n",
|
||
"with code coverage enabled, but it also provides more convenient scripting access to\n",
|
||
"start the coverage engine programmatically. To have coverage metrics nicely integra‐\n",
|
||
"ted into the flask test command added in Chapter 7, a --coverage option can be\n",
|
||
"added. The implementation of this option is shown in Example 15-1.\n",
|
||
"Example 15-1. flasky.py: coverage metrics\n",
|
||
"import os\n",
|
||
"import sys\n",
|
||
"import click\n",
|
||
"COV = None\n",
|
||
"if os.environ.get('FLASK_COVERAGE'):\n",
|
||
" import coverage\n",
|
||
" COV = coverage.coverage(branch=True, include='app/*')\n",
|
||
" COV.start()\n",
|
||
"# ...\n",
|
||
"@app.cli.command()\n",
|
||
"@click.option('--coverage/--no-coverage', default=False,\n",
|
||
" help='Run tests under code coverage.')\n",
|
||
"def test(coverage):\n",
|
||
" \"\"\"Run the unit tests.\"\"\"\n",
|
||
" if coverage and not os.environ.get('FLASK_COVERAGE'):\n",
|
||
" os.environ['FLASK_COVERAGE'] = '1'\n",
|
||
" os.execvp(sys.executable, [sys.executable] + sys.argv)\n",
|
||
" import unittest\n",
|
||
" tests = unittest.TestLoader().discover('tests')\n",
|
||
" unittest.TextTestRunner(verbosity=2).run(tests)\n",
|
||
" if COV:\n",
|
||
" COV.stop()\n",
|
||
" COV.save()\n",
|
||
" print('Coverage Summary:')\n",
|
||
" COV.report()\n",
|
||
" basedir = os.path.abspath(os.path.dirname(__file__))\n",
|
||
" covdir = os.path.join(basedir, 'tmp/coverage')\n",
|
||
" COV.html_report(directory=covdir)\n",
|
||
" print('HTML version: file://%s/index.html' % covdir)\n",
|
||
" COV.erase()\n",
|
||
"The code coverage support is enabled by passing the --coverage option to the flask\n",
|
||
"test command. To add the Boolean option to the test custom command, the\n",
|
||
"222 \n",
|
||
"| \n",
|
||
"Chapter 15: Testing\n",
|
||
"\n",
|
||
"click.option decorator is used. Click then passes the value of the Boolean flag as an\n",
|
||
"argument to the function.\n",
|
||
"But integrating code coverage in the flasky.py script presents a small problem. By the\n",
|
||
"time the --coverage option is received in the test() function, it is already too late to\n",
|
||
"enable coverage metrics; by that time all the code in the global scope has already exe‐\n",
|
||
"cuted. So, to get accurate metrics, the script recursively restarts itself after setting the\n",
|
||
"FLASK_COVERAGE environment variable. In the second run, the top of the script finds\n",
|
||
"that the environment variable is set and turns on coverage from the start, even before\n",
|
||
"all the application imports.\n",
|
||
"The coverage.coverage() function starts the coverage engine. The branch=True\n",
|
||
"option enables branch coverage analysis, which, in addition to tracking which lines of\n",
|
||
"code execute, checks whether for every conditional both the True and False cases\n",
|
||
"have executed. The include option is used to limit coverage analysis to the files that\n",
|
||
"are inside the application package, which is the only code that needs to be measured.\n",
|
||
"Without the include option, all the extensions installed in the virtual environment\n",
|
||
"and the code for the tests itself would be included in the coverage reports—and that\n",
|
||
"would add a lot of noise to the report.\n",
|
||
"After all the tests have executed, the test() function writes a report to the console\n",
|
||
"and also writes a nicer HTML report to disk. The HTML version shows all the source\n",
|
||
"code annotated with colors that indicate the lines that are covered by the tests and the\n",
|
||
"ones that are not.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 15a to check out this version of the applica‐\n",
|
||
"tion. To ensure that you have all the dependencies installed, also\n",
|
||
"run pip install -r requirements/dev.txt.\n",
|
||
"An example of the text-based report follows:\n",
|
||
"(venv) $ flask test --coverage\n",
|
||
"...\n",
|
||
".----------------------------------------------------------------------\n",
|
||
"Ran 23 tests in 6.337s\n",
|
||
"OK\n",
|
||
"Coverage Summary:\n",
|
||
"Name Stmts Miss Branch BrPart Cover\n",
|
||
"----------------------------------------------------------------\n",
|
||
"app/__init__.py 32 0 0 0 100%\n",
|
||
"app/api_v1/__init__.py 3 0 0 0 100%\n",
|
||
"app/api_v1/authentication.py 29 18 10 0 28%\n",
|
||
"app/api_v1/comments.py 40 30 12 0 19%\n",
|
||
"app/api_v1/decorators.py 11 3 2 0 62%\n",
|
||
"Obtaining Code Coverage Reports \n",
|
||
"| \n",
|
||
"223\n",
|
||
"\n",
|
||
"app/api_v1/errors.py 17 10 0 0 41%\n",
|
||
"app/api_v1/posts.py 36 24 8 0 27%\n",
|
||
"app/api_v1/users.py 30 24 12 0 14%\n",
|
||
"app/auth/__init__.py 3 0 0 0 100%\n",
|
||
"app/auth/forms.py 45 8 8 0 70%\n",
|
||
"app/auth/views.py 116 91 42 0 16%\n",
|
||
"app/decorators.py 14 3 2 0 69%\n",
|
||
"app/email.py 15 9 0 0 40%\n",
|
||
"app/exceptions.py 2 0 0 0 100%\n",
|
||
"app/main/__init__.py 6 1 0 0 83%\n",
|
||
"app/main/errors.py 20 15 6 0 19%\n",
|
||
"app/main/forms.py 39 7 6 0 71%\n",
|
||
"app/main/views.py 178 140 34 0 18%\n",
|
||
"app/models.py 236 42 42 6 79%\n",
|
||
"----------------------------------------------------------------\n",
|
||
"TOTAL 872 425 184 6 45%\n",
|
||
"HTML version: file:///home/flask/flasky/tmp/coverage/index.html\n",
|
||
"The report shows an overall coverage of 45%, which is not terrible, but isn’t very good\n",
|
||
"either. The model classes, which have received all the unit testing attention so far,\n",
|
||
"constitute a total of 236 statements, of which 79% are covered in tests. Obviously the\n",
|
||
"views.py files in the main and auth blueprints and the routes in the api_v1 blueprint\n",
|
||
"all have very low coverage, since these are not exercised in any of the existing unit\n",
|
||
"tests. And of course, these coverage metrics are not indicative of how much bug-free\n",
|
||
"code exists in the project, since other factors (such as the quality of the tests) play a\n",
|
||
"big role in that.\n",
|
||
"Armed with this report, it is easy to determine where tests need to be added to the\n",
|
||
"test suite to improve coverage—but unfortunately, not all parts of the application can\n",
|
||
"be tested as easily as the database models. The next two sections discuss more\n",
|
||
"advanced testing strategies that can be applied to view functions, forms, and tem‐\n",
|
||
"plates.\n",
|
||
"The Flask Test Client\n",
|
||
"Some portions of the application code rely heavily on the environment that is created\n",
|
||
"by a running application. For example, you can’t simply invoke the code in a view\n",
|
||
"function to test it, since the function may need to access Flask context variables such\n",
|
||
"as request or session, it may be expecting form data provided in a POST request, and\n",
|
||
"it may also require a logged-in user. In short, view functions can run only within the\n",
|
||
"context of a request and a running application.\n",
|
||
"Flask comes equipped with a test client to try to address this problem, at least to some\n",
|
||
"extent. The test client replicates the environment that exists when an application is\n",
|
||
"running inside a web server, allowing tests to act as clients and send requests.\n",
|
||
"The view functions do not see any major differences when executed under the test\n",
|
||
"client; requests are received and routed to the appropriate view functions, from which\n",
|
||
"224 \n",
|
||
"| \n",
|
||
"Chapter 15: Testing\n",
|
||
"\n",
|
||
"responses are generated and returned. After a view function executes, its response is\n",
|
||
"passed to the test, which can check it for correctness.\n",
|
||
"Testing Web Applications\n",
|
||
"Example 15-2 shows a unit testing framework that uses the test client.\n",
|
||
"Example 15-2. tests/test_client.py: framework for tests using the Flask test client\n",
|
||
"import unittest\n",
|
||
"from app import create_app, db\n",
|
||
"from app.models import User, Role\n",
|
||
"class FlaskClientTestCase(unittest.TestCase):\n",
|
||
" def setUp(self):\n",
|
||
" self.app = create_app('testing')\n",
|
||
" self.app_context = self.app.app_context()\n",
|
||
" self.app_context.push()\n",
|
||
" db.create_all()\n",
|
||
" Role.insert_roles()\n",
|
||
" self.client = self.app.test_client(use_cookies=True)\n",
|
||
" def tearDown(self):\n",
|
||
" db.session.remove()\n",
|
||
" db.drop_all()\n",
|
||
" self.app_context.pop()\n",
|
||
" def test_home_page(self):\n",
|
||
" response = self.client.get('/')\n",
|
||
" self.assertEqual(response.status_code, 200)\n",
|
||
" self.assertTrue('Stranger' in response.get_data(as_text=True))\n",
|
||
"Compared to tests/test_basics.py, this module adds a self.client instance variable,\n",
|
||
"which is the Flask test client object. This object exposes methods that issue requests\n",
|
||
"into the application. When the test client is created with the use_cookies option\n",
|
||
"enabled, it will accept and send cookies in the same way browsers do, so functionality\n",
|
||
"that relies on cookies to recall context between requests can be used. In particular,\n",
|
||
"this approach enables the use of user sessions, which are stored in cookies.\n",
|
||
"The test_home_page() test is a simple example of what the test client can do. In this\n",
|
||
"example, a request for the root URL of the application is issued. The return value of\n",
|
||
"the get() method of the test client is a Flask response object containing the response\n",
|
||
"returned by the invoked view function. To check whether the test was successful, the\n",
|
||
"status code of the response is checked, and then the body of the response, obtained\n",
|
||
"from response.get_data(), is searched for the word \"Stranger\", which is part of\n",
|
||
"the “Hello, Stranger!” greeting shown to anonymous users. Note that get_data()\n",
|
||
"The Flask Test Client \n",
|
||
"| \n",
|
||
"225\n",
|
||
"\n",
|
||
"returns the response body as a byte array by default; passing as_text=True converts it\n",
|
||
"to a string, which is easier to work with.\n",
|
||
"The test client can also send POST requests that include form data using the post()\n",
|
||
"method, but submitting forms presents a small complication. As discussed in Chap‐\n",
|
||
"ter 4, all forms generated by Flask-WTF have a hidden field with a CSRF token that\n",
|
||
"needs to be submitted along with the form. To be able to send the CSRF token, a test\n",
|
||
"would need to request the page that displays the form, then parse the HTML returned\n",
|
||
"in that response and extract the token, so that it can then send it with the form data.\n",
|
||
"To avoid the hassle of dealing with CSRF tokens in tests, it is better to disable CSRF\n",
|
||
"protection in the testing configuration. This is shown in Example 15-3.\n",
|
||
"Example 15-3. config.py: disabling CSRF protection in the testing configuration\n",
|
||
"class TestingConfig(Config):\n",
|
||
" #...\n",
|
||
" WTF_CSRF_ENABLED = False\n",
|
||
"Example 15-4 shows a more advanced unit test that simulates a new user registering\n",
|
||
"an account, logging in, confirming the account with a confirmation token, and finally\n",
|
||
"logging out.\n",
|
||
"Example 15-4. tests/test_client.py: simulation of a new user workflow with the Flask test\n",
|
||
"client\n",
|
||
"class FlaskClientTestCase(unittest.TestCase):\n",
|
||
" # ...\n",
|
||
" def test_register_and_login(self):\n",
|
||
" # register a new account\n",
|
||
" response = self.client.post('/auth/register', data={\n",
|
||
" 'email': 'john@example.com',\n",
|
||
" 'username': 'john',\n",
|
||
" 'password': 'cat',\n",
|
||
" 'password2': 'cat'\n",
|
||
" })\n",
|
||
" self.assertEqual(response.status_code, 302)\n",
|
||
" # log in with the new account\n",
|
||
" response = self.client.post('/auth/login', data={\n",
|
||
" 'email': 'john@example.com',\n",
|
||
" 'password': 'cat'\n",
|
||
" }, follow_redirects=True)\n",
|
||
" self.assertEqual(response.status_code, 200)\n",
|
||
" self.assertTrue(re.search('Hello,\\s+john!',\n",
|
||
" response.get_data(as_text=True)))\n",
|
||
" self.assertTrue(\n",
|
||
" 'You have not confirmed your account yet' in response.get_data(\n",
|
||
" as_text=True))\n",
|
||
"226 \n",
|
||
"| \n",
|
||
"Chapter 15: Testing\n",
|
||
"\n",
|
||
" # send a confirmation token\n",
|
||
" user = User.query.filter_by(email='john@example.com').first()\n",
|
||
" token = user.generate_confirmation_token()\n",
|
||
" response = self.client.get('/auth/confirm/{}'.format(token),\n",
|
||
" follow_redirects=True)\n",
|
||
" user.confirm(token)\n",
|
||
" self.assertEqual(response.status_code, 200)\n",
|
||
" self.assertTrue(\n",
|
||
" 'You have confirmed your account' in response.get_data(\n",
|
||
" as_text=True))\n",
|
||
" # log out\n",
|
||
" response = self.client.get('/auth/logout', follow_redirects=True)\n",
|
||
" self.assertEqual(response.status_code, 200)\n",
|
||
" self.assertTrue('You have been logged out' in response.get_data(\n",
|
||
" as_text=True))\n",
|
||
"The test begins with a form submission to the registration route. The data argument\n",
|
||
"to post() is a dictionary with the form fields, which must exactly match the field\n",
|
||
"names defined in the HTML form. Since CSRF protection is now disabled in the test‐\n",
|
||
"ing configuration, there is no need to send the CSRF token with the form.\n",
|
||
"The /auth/register route can respond in two ways. If the registration data is valid, a\n",
|
||
"redirect sends the user to the login page. In the case of an invalid registration, the\n",
|
||
"response renders the page with the registration form again, including any appropriate\n",
|
||
"error messages. To validate that the registration was accepted, the test checks that the\n",
|
||
"status code of the response is 302, which is the code for a redirect.\n",
|
||
"The second section of the test issues a login request to the application using the email\n",
|
||
"and password just registered. This is done with a POST request to the /auth/login\n",
|
||
"route. This time a follow_redirects=True argument is included in the post() call to\n",
|
||
"make the test client work like a browser and automatically issue a GET request for the\n",
|
||
"redirected URL. With this option, status code 302 will not be returned; instead, the\n",
|
||
"response from the redirected URL is returned.\n",
|
||
"A successful response to the login submission would now have a page that greets the\n",
|
||
"user by their username and then indicates that the account needs to be confirmed to\n",
|
||
"gain access. Two assert statements verify that this is the page returned. Here, it is\n",
|
||
"interesting to note that a search for the string 'Hello, john!' would not work\n",
|
||
"because this string is assembled from static and dynamic portions, so due to the way\n",
|
||
"the Jinja2 template was created the final HTML has extra whitespace in between these\n",
|
||
"two words. To avoid an error in this test due to the whitespace, a regular expression is\n",
|
||
"used.\n",
|
||
"The next step is to confirm the account, which presents another small obstacle. The\n",
|
||
"confirmation URL is sent to the user by email during registration, so there is no easy\n",
|
||
"way to access it from the test. The solution presented in the test bypasses the token\n",
|
||
"The Flask Test Client \n",
|
||
"| \n",
|
||
"227\n",
|
||
"\n",
|
||
"that was generated as part of the registration and generates another one directly from\n",
|
||
"the User instance. Another possibility would have been to extract the token by pars‐\n",
|
||
"ing the email body, which Flask-Mail saves when running in a testing configuration.\n",
|
||
"With the token at hand, the next step of the test is to simulate the user clicking the\n",
|
||
"confirmation token URL received by email. This is achieved by sending a GET request\n",
|
||
"to the confirmation URL, which includes the token. The response to this request is a\n",
|
||
"redirect to the home page, but once again follow_redirects=True is specified, so the\n",
|
||
"test client requests the redirected page automatically and returns it. The response is\n",
|
||
"checked for the greeting and a flashed message that informs the user that the confir‐\n",
|
||
"mation was successful.\n",
|
||
"The final step in this test is to send a GET request to the logout route; to confirm that\n",
|
||
"this has worked, the test searches for the flashed message in the response.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 15b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Testing Web Services\n",
|
||
"The Flask test client can also be used to test RESTful web services. Example 15-5\n",
|
||
"shows an example unit test class with two tests.\n",
|
||
"Example 15-5. tests/test_api.py: RESTful API testing with the Flask test client\n",
|
||
"class APITestCase(unittest.TestCase):\n",
|
||
" # ...\n",
|
||
" def get_api_headers(self, username, password):\n",
|
||
" return {\n",
|
||
" 'Authorization':\n",
|
||
" 'Basic ' + b64encode(\n",
|
||
" (username + ':' + password).encode('utf-8')).decode('utf-8'),\n",
|
||
" 'Accept': 'application/json',\n",
|
||
" 'Content-Type': 'application/json'\n",
|
||
" }\n",
|
||
" def test_no_auth(self):\n",
|
||
" response = self.client.get(url_for('api.get_posts'),\n",
|
||
" content_type='application/json')\n",
|
||
" self.assertEqual(response.status_code, 401)\n",
|
||
" def test_posts(self):\n",
|
||
" # add a user\n",
|
||
" r = Role.query.filter_by(name='User').first()\n",
|
||
" self.assertIsNotNone(r)\n",
|
||
"228 \n",
|
||
"| \n",
|
||
"Chapter 15: Testing\n",
|
||
"\n",
|
||
" u = User(email='john@example.com', password='cat', confirmed=True,\n",
|
||
" role=r)\n",
|
||
" db.session.add(u)\n",
|
||
" db.session.commit()\n",
|
||
" # write a post\n",
|
||
" response = self.client.post(\n",
|
||
" '/api/v1/posts/',\n",
|
||
" headers=self.get_api_headers('john@example.com', 'cat'),\n",
|
||
" data=json.dumps({'body': 'body of the *blog* post'}))\n",
|
||
" self.assertEqual(response.status_code, 201)\n",
|
||
" url = response.headers.get('Location')\n",
|
||
" self.assertIsNotNone(url)\n",
|
||
" # get the new post\n",
|
||
" response = self.client.get(\n",
|
||
" url,\n",
|
||
" headers=self.get_api_headers('john@example.com', 'cat'))\n",
|
||
" self.assertEqual(response.status_code, 200)\n",
|
||
" json_response = json.loads(response.get_data(as_text=True))\n",
|
||
" self.assertEqual('http://localhost' + json_response['url'], url)\n",
|
||
" self.assertEqual(json_response['body'], 'body of the *blog* post')\n",
|
||
" self.assertEqual(json_response['body_html'],\n",
|
||
" '<p>body of the <em>blog</em> post</p>')\n",
|
||
"The setUp() and tearDown() methods for testing the API are the same as for the reg‐\n",
|
||
"ular application, but the cookie support does not need to be configured because the\n",
|
||
"API does not use it. The get_api_headers() method is a helper method that returns\n",
|
||
"the common headers that need to be sent with most API requests. These include the\n",
|
||
"authentication credentials and the MIME type-related headers.\n",
|
||
"The test_no_auth() test is a simple test that ensures that a request that does not\n",
|
||
"include authentication credentials is rejected with error code 401. The test_posts()\n",
|
||
"test adds a user to the database and then uses the RESTful API to insert a blog post\n",
|
||
"and then read it back. Any requests that send data in the body must encode it with\n",
|
||
"json.dumps(), because the Flask test client does not automatically encode to JSON.\n",
|
||
"Likewise, response bodies are also returned in JSON format and must be decoded\n",
|
||
"with json.loads() before they can be inspected.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 15c to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"The Flask Test Client \n",
|
||
"| \n",
|
||
"229\n",
|
||
"\n",
|
||
"End-to-End Testing with Selenium\n",
|
||
"The Flask test client cannot fully emulate the environment of a running application.\n",
|
||
"For example, any application that relies on JavaScript code running in the client\n",
|
||
"browser will not work, as the JavaScript code included in the responses will be\n",
|
||
"returned to the test without being executed.\n",
|
||
"When tests require the complete environment, there is no other choice than to use a\n",
|
||
"real web browser connected to the application running on a real web server. Fortu‐\n",
|
||
"nately, most web browsers can be automated. Selenium is a web browser automation\n",
|
||
"tool that supports the most popular web browsers in the three major operating sys‐\n",
|
||
"tems.\n",
|
||
"The Python interface for Selenium is installed with pip:\n",
|
||
"(venv) $ pip install selenium\n",
|
||
"Selenium requires a driver for the desired web browser to be installed separately, in\n",
|
||
"addition to the browser itself. There are drivers for all major web browsers, so an\n",
|
||
"application could set up a sophisticated framework to test several browsers. For this\n",
|
||
"application, however, only the Google Chrome web browser will be used for automa‐\n",
|
||
"ted tests, with its corresponding driver, ChromeDriver. If you are using a macOS\n",
|
||
"computer with the brew package installer, you can install ChromeDriver as follows:\n",
|
||
"(venv) $ brew install chromedriver\n",
|
||
"For Linux, Microsoft Windows, or a macOS computer without brew, you can down‐\n",
|
||
"load a regular ChromeDriver installer from the ChromeDriver website.\n",
|
||
"Testing with Selenium requires the application to be running inside a web server that\n",
|
||
"is listening for real HTTP requests. The method that will be shown in this section\n",
|
||
"starts the application with the development server in a background thread while the\n",
|
||
"tests run on the main thread. Under the control of the tests, Selenium launches a web\n",
|
||
"browser and makes it connect to the application to perform the required operations.\n",
|
||
"A problem with this approach is that after all the tests have completed, the Flask\n",
|
||
"server needs to be stopped, ideally in a graceful way, so that background tasks such as\n",
|
||
"the code coverage engine can cleanly complete their work. The Werkzeug web server\n",
|
||
"has a shutdown option, but because the server is running isolated in its own thread,\n",
|
||
"the only way to ask the server to shut down is by sending a regular HTTP request.\n",
|
||
"Example 15-6 shows the implementation of a server shutdown route.\n",
|
||
"230 \n",
|
||
"| \n",
|
||
"Chapter 15: Testing\n",
|
||
"\n",
|
||
"Example 15-6. _app/main/views.py: server shutdown route\n",
|
||
"@main.route('/shutdown')\n",
|
||
"def server_shutdown():\n",
|
||
" if not current_app.testing:\n",
|
||
" abort(404)\n",
|
||
" shutdown = request.environ.get('werkzeug.server.shutdown')\n",
|
||
" if not shutdown:\n",
|
||
" abort(500)\n",
|
||
" shutdown()\n",
|
||
" return 'Shutting down...'\n",
|
||
"The shutdown route will work only when the application is running in testing mode;\n",
|
||
"invoking it in other configurations will return a 404 status code response. The actual\n",
|
||
"shutdown procedure involves calling a shutdown function that Werkzeug exposes in\n",
|
||
"the environment. After calling this function and returning from the request, the\n",
|
||
"development web server will know that it needs to exit gracefully.\n",
|
||
"Example 15-7 shows the layout of a test case that is configured to run tests with Sele‐\n",
|
||
"nium.\n",
|
||
"Example 15-7. tests/test_selenium.py: framework for tests using Selenium\n",
|
||
"from selenium import webdriver\n",
|
||
"class SeleniumTestCase(unittest.TestCase):\n",
|
||
" client = None\n",
|
||
" @classmethod\n",
|
||
" def setUpClass(cls):\n",
|
||
" # start Chrome\n",
|
||
" options = webdriver.ChromeOptions()\n",
|
||
" options.add_argument('headless')\n",
|
||
" try:\n",
|
||
" cls.client = webdriver.Chrome(chrome_options=options)\n",
|
||
" except:\n",
|
||
" pass\n",
|
||
" # skip these tests if the browser could not be started\n",
|
||
" if cls.client:\n",
|
||
" # create the application\n",
|
||
" cls.app = create_app('testing')\n",
|
||
" cls.app_context = cls.app.app_context()\n",
|
||
" cls.app_context.push()\n",
|
||
" # suppress logging to keep unittest output clean\n",
|
||
" import logging\n",
|
||
" logger = logging.getLogger('werkzeug')\n",
|
||
" logger.setLevel(\"ERROR\")\n",
|
||
"End-to-End Testing with Selenium \n",
|
||
"| \n",
|
||
"231\n",
|
||
"\n",
|
||
" # create the database and populate with some fake data\n",
|
||
" db.create_all()\n",
|
||
" Role.insert_roles()\n",
|
||
" fake.users(10)\n",
|
||
" fake.posts(10)\n",
|
||
" # add an administrator user\n",
|
||
" admin_role = Role.query.filter_by(permissions=0xff).first()\n",
|
||
" admin = User(email='john@example.com',\n",
|
||
" username='john', password='cat',\n",
|
||
" role=admin_role, confirmed=True)\n",
|
||
" db.session.add(admin)\n",
|
||
" db.session.commit()\n",
|
||
" # start the Flask server in a thread\n",
|
||
" cls.server_thread = threading.Thread(\n",
|
||
" target=cls.app.run, kwargs={'debug': 'false',\n",
|
||
" 'use_reloader': False,\n",
|
||
" 'use_debugger': False})\n",
|
||
" cls.server_thread.start()\n",
|
||
" @classmethod\n",
|
||
" def tearDownClass(cls):\n",
|
||
" if cls.client:\n",
|
||
" # stop the Flask server and the browser\n",
|
||
" cls.client.get('http://localhost:5000/shutdown')\n",
|
||
" cls.client.quit()\n",
|
||
" cls.server_thread.join()\n",
|
||
" # destroy database\n",
|
||
" db.drop_all()\n",
|
||
" db.session.remove()\n",
|
||
" # remove application context\n",
|
||
" cls.app_context.pop()\n",
|
||
" def setUp(self):\n",
|
||
" if not self.client:\n",
|
||
" self.skipTest('Web browser not available')\n",
|
||
" def tearDown(self):\n",
|
||
" pass\n",
|
||
"The setUpClass() and tearDownClass() class methods are invoked before and after\n",
|
||
"the tests in this class execute. The setup involves starting an instance of Chrome\n",
|
||
"through Selenium’s webdriver API, and creating an application and a database with\n",
|
||
"some initial fake data for tests to use. The application is started in a thread using the\n",
|
||
"app.run() method. At the end the application receives a request to /shutdown, which\n",
|
||
"causes the background thread to end. The browser is then closed and the test data‐\n",
|
||
"base removed.\n",
|
||
"232 \n",
|
||
"| \n",
|
||
"Chapter 15: Testing\n",
|
||
"\n",
|
||
"Before the Flask command-line interface based on Click was intro‐\n",
|
||
"duced, you had to start the Flask development web server by call‐\n",
|
||
"ing app.run() from the application’s main script, or else use a\n",
|
||
"third-party extension such as Flask-Script. While using app.run()\n",
|
||
"to start a server is now replaced with the flask run command, the\n",
|
||
"app.run() method continues to be supported, and here you can\n",
|
||
"see how it can still be useful for complex unit testing situations.\n",
|
||
"Selenium supports many other web browsers besides Chrome.\n",
|
||
"Consult the Selenium documentation if you wish to use another\n",
|
||
"web browser or test additional browsers.\n",
|
||
"The setUp() method that runs before each test skips tests if Selenium cannot start the\n",
|
||
"web browser in the startUpClass() method. In Example 15-8 you can see an exam‐\n",
|
||
"ple test built with Selenium.\n",
|
||
"Example 15-8. tests/test_selenium.py: example Selenium unit test\n",
|
||
"class SeleniumTestCase(unittest.TestCase):\n",
|
||
" # ...\n",
|
||
" def test_admin_home_page(self):\n",
|
||
" # navigate to home page\n",
|
||
" self.client.get('http://localhost:5000/')\n",
|
||
" self.assertTrue(re.search('Hello,\\s+Stranger!',\n",
|
||
" self.client.page_source))\n",
|
||
" # navigate to login page\n",
|
||
" self.client.find_element_by_link_text('Log In').click()\n",
|
||
" self.assertIn('<h1>Login</h1>', self.client.page_source)\n",
|
||
" # log in\n",
|
||
" self.client.find_element_by_name('email').\\\n",
|
||
" send_keys('john@example.com')\n",
|
||
" self.client.find_element_by_name('password').send_keys('cat')\n",
|
||
" self.client.find_element_by_name('submit').click()\n",
|
||
" self.assertTrue(re.search('Hello,\\s+john!', self.client.page_source))\n",
|
||
" # navigate to the user's profile page\n",
|
||
" self.client.find_element_by_link_text('Profile').click()\n",
|
||
" self.assertIn('<h1>john</h1>', self.client.page_source)\n",
|
||
"This test logs in to the application using the administrator account that was created in\n",
|
||
"setUpClass() and then opens the user’s profile page. Note how different the testing\n",
|
||
"methodology is from the Flask test client. When testing with Selenium, tests send\n",
|
||
"commands to the web browser and never interact with the application directly. The\n",
|
||
"End-to-End Testing with Selenium \n",
|
||
"| \n",
|
||
"233\n",
|
||
"\n",
|
||
"commands closely match the actions that a real user would perform with a mouse or\n",
|
||
"keyboard.\n",
|
||
"The test begins with a call to get() with the home page of the application. In the\n",
|
||
"browser, this causes the URL to be entered in the address bar. To verify this step, the\n",
|
||
"page source is checked for the “Hello, Stranger!” greeting.\n",
|
||
"To go to the sign-in page, the test looks for the “Log In” link using\n",
|
||
"find_element_by_link_text() and then calls click() on it to trigger a real click in\n",
|
||
"the browser. Selenium provides several find_element_by...() convenience methods\n",
|
||
"that can search for elements within the HTML page in different ways.\n",
|
||
"To log in to the application, the test locates the email and password form fields by\n",
|
||
"their names using find_element_by_name() and then writes text into them with\n",
|
||
"send_keys(). The form is submitted by calling click() on the submit button. The\n",
|
||
"personalized greeting is checked to ensure that the login was successful and the\n",
|
||
"browser is now on the home page.\n",
|
||
"The final part of the test locates the “Profile” link in the navigation bar and clicks it.\n",
|
||
"To verify that the profile page was loaded, the heading with the username is searched\n",
|
||
"in the page source.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 15d to check out this version of the applica‐\n",
|
||
"tion. This update contains a database migration, so remember to\n",
|
||
"run flask db upgrade after you check out the code. To ensure that\n",
|
||
"you have all the dependencies installed, also run pip install -r\n",
|
||
"requirements/dev.txt.\n",
|
||
"When you run the unit tests with the flask test command there will be no visible\n",
|
||
"difference. The test_admin_home_page unit test in Example 15-8 will run a headless\n",
|
||
"Chrome instance and perform all the actions on it. If you want to see the actions per‐\n",
|
||
"formed \n",
|
||
"in \n",
|
||
"a \n",
|
||
"real \n",
|
||
"Chrome \n",
|
||
"window, \n",
|
||
"comment \n",
|
||
"out \n",
|
||
"the \n",
|
||
"line\n",
|
||
"options.add_argument('headless') in the setUpClass() method, so that Selenium\n",
|
||
"creates a regular Chrome window.\n",
|
||
"Is It Worth It?\n",
|
||
"By now you may be asking yourself if testing using the Flask test client or Selenium is\n",
|
||
"really worth the trouble. It is a valid question, and it does not have a simple answer.\n",
|
||
"Whether you like it or not, your application will be tested. If you don’t test it yourself,\n",
|
||
"then your users will become the unwilling testers; they will find the bugs, and then\n",
|
||
"you will have to fix them under pressure. Simple and focused tests like the ones that\n",
|
||
"exercise database models and other parts of the application that can be executed out‐\n",
|
||
"234 \n",
|
||
"| \n",
|
||
"Chapter 15: Testing\n",
|
||
"\n",
|
||
"side of the context of an application should always be carried out, as they have a very\n",
|
||
"low cost and ensure the proper functioning of the core pieces of application logic.\n",
|
||
"End-to-end tests of the type that the Flask test client and Selenium can carry out are\n",
|
||
"sometimes necessary, but due to the increased complexity of writing them, they\n",
|
||
"should be used only for functionality that cannot be tested in isolation. The applica‐\n",
|
||
"tion code should be organized so that it is possible to push the business logic into\n",
|
||
"application modules that are independent of the context of the application, and thus\n",
|
||
"can be tested more easily. The code that exists in view functions should be simple and\n",
|
||
"just act as a thin layer that accepts requests and invokes the corresponding actions in\n",
|
||
"other classes or functions that encapsulate the application logic.\n",
|
||
"So yes, testing is absolutely worth it. But it is important to design an efficient testing\n",
|
||
"strategy and write code that can take advantage of it.\n",
|
||
"Is It Worth It? \n",
|
||
"| \n",
|
||
"235\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 16\n",
|
||
"Performance\n",
|
||
"Nobody likes slow applications. Long waits for pages to load frustrate users, so it is\n",
|
||
"important to detect and correct performance problems as soon as they appear. In this\n",
|
||
"chapter, two important performance aspects of web applications are considered.\n",
|
||
"Logging Slow Database Performance\n",
|
||
"When application performance slowly degenerates with time, it is likely due to slow\n",
|
||
"database queries, which get worse as the size of the database grows. Optimizing data‐\n",
|
||
"base queries can be as simple as adding more indexes or as complex as adding a cache\n",
|
||
"between the application and the database. The explain statement, available in most\n",
|
||
"database query languages, shows the steps the database takes to execute a given query,\n",
|
||
"often exposing inefficiencies in database or index design.\n",
|
||
"But before starting to optimize queries, it is necessary to determine which queries are\n",
|
||
"the ones that are worth optimizing. During a typical request several database queries\n",
|
||
"may be issued, so it is often hard to identify which of all the queries are the slow ones.\n",
|
||
"Flask-SQLAlchemy has an option to record statistics about database queries issued\n",
|
||
"during a request. In Example 16-1 you can see how this feature can be used to log\n",
|
||
"queries that are slower than a configured threshold.\n",
|
||
"Example 16-1. app/main/views.py: reporting slow database queries\n",
|
||
"from flask_sqlalchemy import get_debug_queries\n",
|
||
"@main.after_app_request\n",
|
||
"def after_request(response):\n",
|
||
" for query in get_debug_queries():\n",
|
||
" if query.duration >= current_app.config['FLASKY_SLOW_DB_QUERY_TIME']:\n",
|
||
" current_app.logger.warning(\n",
|
||
"237\n",
|
||
"\n",
|
||
" 'Slow query: %s\\nParameters: %s\\nDuration: %fs\\nContext: %s\\n' %\n",
|
||
" (query.statement, query.parameters, query.duration,\n",
|
||
" query.context))\n",
|
||
" return response\n",
|
||
"This functionality is attached to an after_app_request handler, which works in a\n",
|
||
"similar way to the before_app_request handler but is invoked after the view func‐\n",
|
||
"tion that handles the request returns. Flask passes the response object to the\n",
|
||
"after_app_request handler in case it needs to be modified.\n",
|
||
"In this case, the after_app_request handler does not modify the response; it just\n",
|
||
"gets the query timings recorded by Flask-SQLAlchemy and then logs the slow ones to\n",
|
||
"the application logger that Flask sets up at app.logger, before returning the response,\n",
|
||
"which will then be sent to the client.\n",
|
||
"The get_debug_queries() function returns the queries issued during the request as a\n",
|
||
"list. The information provided for each query is shown in Table 16-1.\n",
|
||
"Table 16-1. Query statistics recorded by Flask-SQLAlchemy\n",
|
||
"Name\n",
|
||
"Description\n",
|
||
"statement\n",
|
||
"The SQL statement\n",
|
||
"parameters\n",
|
||
"The parameters used with the SQL statement\n",
|
||
"start_time The time the query was issued\n",
|
||
"end_time\n",
|
||
"The time the query returned\n",
|
||
"duration\n",
|
||
"The duration of the query in seconds\n",
|
||
"context\n",
|
||
"A string that indicates the source code location where the query was issued\n",
|
||
"The after_app_request handler walks the list and logs any queries that lasted longer\n",
|
||
"than a threshold given in the configuration variable FLASKY_SLOW_DB_QUERY_TIME.\n",
|
||
"The logging is issued at the warning level in this application, but in some cases it may\n",
|
||
"make sense to treat slow database alerts as errors.\n",
|
||
"The get_debug_queries() function is enabled only in debug mode by default.\n",
|
||
"Unfortunately, database performance problems rarely show up during development\n",
|
||
"because much smaller databases are used. For this reason, it is much more useful to\n",
|
||
"enable this option in production. Example 16-2 shows the configuration changes that\n",
|
||
"are necessary to enable database query performance monitoring in production mode.\n",
|
||
"238 \n",
|
||
"| \n",
|
||
"Chapter 16: Performance\n",
|
||
"\n",
|
||
"Example 16-2. config.py: configuration for slow query reporting\n",
|
||
"class Config:\n",
|
||
" # ...\n",
|
||
" SQLALCHEMY_RECORD_QUERIES = True\n",
|
||
" FLASKY_SLOW_DB_QUERY_TIME = 0.5\n",
|
||
" # ...\n",
|
||
"SQLALCHEMY_RECORD_QUERIES tells Flask-SQLAlchemy to enable the recording of\n",
|
||
"query statistics. The slow query threshold is set to half a second. Both configuration\n",
|
||
"variables were included in the base Config class, so they will be enabled for all config‐\n",
|
||
"urations.\n",
|
||
"Whenever a slow query is detected, an entry will be written to Flask’s application log‐\n",
|
||
"ger. To be able to store these log entries, the logger must be configured. The logging\n",
|
||
"configuration largely depends on the platform that hosts the application. Some exam‐\n",
|
||
"ples are shown in Chapter 17.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 16a to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Source Code Profiling\n",
|
||
"Another possible source of performance problems is high CPU consumption, caused\n",
|
||
"by functions that perform heavy computing. Source code profilers are useful in find‐\n",
|
||
"ing the slowest parts of an application. A profiler watches a running application and\n",
|
||
"records the functions that are called and how long each takes to run. It then produces\n",
|
||
"a detailed report showing the slowest functions.\n",
|
||
"Profiling is typically done only in a development environment. A\n",
|
||
"source code profiler makes the application run much slower than\n",
|
||
"normal, because it has to observe and take notes on all that is hap‐\n",
|
||
"pening in real time. Profiling on a production system is not recom‐\n",
|
||
"mended, unless a lightweight profiler specifically designed to run in\n",
|
||
"a production environment is used.\n",
|
||
"Flask’s development web server, which comes from Werkzeug, can optionally enable\n",
|
||
"the Python profiler for each request. Example 16-3 adds a new command-line option\n",
|
||
"to the application that starts the web server under the profiler.\n",
|
||
"Source Code Profiling \n",
|
||
"| \n",
|
||
"239\n",
|
||
"\n",
|
||
"Example 16-3. flasky.py: running the application under the request profiler\n",
|
||
"@app.cli.command()\n",
|
||
"@click.option('--length', default=25,\n",
|
||
" help='Number of functions to include in the profiler report.')\n",
|
||
"@click.option('--profile-dir', default=None,\n",
|
||
" help='Directory where profiler data files are saved.')\n",
|
||
"def profile(length, profile_dir):\n",
|
||
" \"\"\"Start the application under the code profiler.\"\"\"\n",
|
||
" from werkzeug.contrib.profiler import ProfilerMiddleware\n",
|
||
" app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],\n",
|
||
" profile_dir=profile_dir)\n",
|
||
" app.run(debug=False)\n",
|
||
"This command attaches the ProfilerMiddleware from Werkzeug to the application,\n",
|
||
"through its wsgi_app attribute. WSGI middlewares are invoked each time the web\n",
|
||
"server dispatches a request to the application and can modify the way the request is\n",
|
||
"handled, in this case by capturing profiling data. Note that the application is then\n",
|
||
"started programmatically using the app.run() method.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 16b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"When the application is started with flask profile, the console will show the pro‐\n",
|
||
"filer statistics for each request, which will include the slowest 25 functions. The\n",
|
||
"--length option can be used to change the number of functions shown in the report.\n",
|
||
"If the --profile-dir option is given, the profile data for each request is saved to a file\n",
|
||
"in the given directory. The profiler data files can be used to generate more detailed\n",
|
||
"reports that include a call graph. For more information on the Python profiler, con‐\n",
|
||
"sult the official documentation.\n",
|
||
"The preparations for deployment are complete. The next chapter will give you an\n",
|
||
"overview of what to expect when deploying your application.\n",
|
||
"240 \n",
|
||
"| \n",
|
||
"Chapter 16: Performance\n",
|
||
"\n",
|
||
"CHAPTER 17\n",
|
||
"Deployment\n",
|
||
"The web development server that comes bundled with Flask is not robust, secure, or\n",
|
||
"efficient enough to work in a production environment. In this chapter, production\n",
|
||
"deployment options for Flask applications are examined.\n",
|
||
"Deployment Workflow\n",
|
||
"Regardless of the hosting method used, there are a series of tasks that must be carried\n",
|
||
"out when the application is installed on a production server. These include the cre‐\n",
|
||
"ation or update of the database tables.\n",
|
||
"Having to run these tasks manually each time the application is installed or upgraded\n",
|
||
"is error prone and time consuming. Instead, a command that performs all the\n",
|
||
"required tasks can be added to flasky.py.\n",
|
||
"Example 17-1 shows a deploy command implementation that is appropriate for\n",
|
||
"Flasky.\n",
|
||
"Example 17-1. flasky.py: deploy command\n",
|
||
"from flask_migrate import upgrade\n",
|
||
"from app.models import Role, User\n",
|
||
"@manager.command\n",
|
||
"def deploy():\n",
|
||
" \"\"\"Run deployment tasks.\"\"\"\n",
|
||
" # migrate database to latest revision\n",
|
||
" upgrade()\n",
|
||
" # create or update user roles\n",
|
||
" Role.insert_roles()\n",
|
||
"241\n",
|
||
"\n",
|
||
" # ensure all users are following themselves\n",
|
||
" User.add_self_follows()\n",
|
||
"The functions invoked by this command were all created before; they are just invoked\n",
|
||
"all together from a single command to simplify the deployment of the application.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 17a to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"These functions are all designed in a way that causes no problems if they are executed\n",
|
||
"multiple times. Designing update functions in this way makes it possible to run just\n",
|
||
"this deploy command every time an installation or upgrade is done without having to\n",
|
||
"worry about side effects caused by a function that runs at the wrong time.\n",
|
||
"Logging of Errors During Production\n",
|
||
"When the application is running in debug mode, Werkzeug’s interactive debugger\n",
|
||
"appears whenever an error occurs. The stack trace of the error is displayed on the web\n",
|
||
"page, and it is possible to look at the source code and even evaluate expressions in the\n",
|
||
"context of each stack frame using Flask’s interactive web-based debugger.\n",
|
||
"The debugger is an excellent tool to debug application problems during development,\n",
|
||
"but obviously it cannot be used in a production deployment. Errors that occur in pro‐\n",
|
||
"duction are silenced and instead the user receives a discrete code 500 error page. But\n",
|
||
"luckily, the stack traces of these errors are not completely lost, as Flask writes them to\n",
|
||
"a log file.\n",
|
||
"During startup, Flask creates an instance of Python’s logging.Logger class and\n",
|
||
"attaches it to the application instance as app.logger. In debug mode, this logger\n",
|
||
"writes to the console, but in production mode there are no handlers configured for it\n",
|
||
"by default. Unless a handler is added, logs are not stored. The changes in\n",
|
||
"Example 17-2 configure a logging handler that sends the errors that occur while run‐\n",
|
||
"ning under the production configuration to the administrator email address config‐\n",
|
||
"ured in the FLASKY_ADMIN setting.\n",
|
||
"Example 17-2. config.py: sending email for application errors\n",
|
||
"class ProductionConfig(Config):\n",
|
||
" # ...\n",
|
||
" @classmethod\n",
|
||
" def init_app(cls, app):\n",
|
||
" Config.init_app(app)\n",
|
||
"242 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
" # email errors to the administrators\n",
|
||
" import logging\n",
|
||
" from logging.handlers import SMTPHandler\n",
|
||
" credentials = None\n",
|
||
" secure = None\n",
|
||
" if getattr(cls, 'MAIL_USERNAME', None) is not None:\n",
|
||
" credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)\n",
|
||
" if getattr(cls, 'MAIL_USE_TLS', None):\n",
|
||
" secure = ()\n",
|
||
" mail_handler = SMTPHandler(\n",
|
||
" mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),\n",
|
||
" fromaddr=cls.FLASKY_MAIL_SENDER,\n",
|
||
" toaddrs=[cls.FLASKY_ADMIN],\n",
|
||
" subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error',\n",
|
||
" credentials=credentials,\n",
|
||
" secure=secure)\n",
|
||
" mail_handler.setLevel(logging.ERROR)\n",
|
||
" app.logger.addHandler(mail_handler)\n",
|
||
"Recall that all configuration classes have an init_app() static method that is invoked\n",
|
||
"by create_app(), which so far has not been used. In the implementation of this\n",
|
||
"method for the ProductionConfig class, the application logger is now configured\n",
|
||
"with a log handler that sends errors to an email recipient.\n",
|
||
"The logging level of the email logger is set to logging.ERROR, so only severe problems\n",
|
||
"are going to be emailed. Messages logged on lesser levels can be logged to a file,\n",
|
||
"syslog, or any other supported destination by adding the proper logging handlers.\n",
|
||
"The logging method to use for these messages largely depends on the hosting plat‐\n",
|
||
"form.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 17b to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Cloud Deployment\n",
|
||
"The trend in application hosting is to host “in the cloud,” but this can mean many\n",
|
||
"different things. At the most basic level, cloud hosting can mean that the application\n",
|
||
"is installed on one or more virtual servers, which for all intents and purposes operate\n",
|
||
"and feel like physical machines, but in reality are virtual machines managed by the\n",
|
||
"cloud operator. An example of these types of servers are those available through the\n",
|
||
"EC2 service from Amazon Web Services (AWS). Deploying an application to a virtual\n",
|
||
"server is similar to doing a traditional deployment to a dedicated server, as described\n",
|
||
"later in this chapter.\n",
|
||
"Cloud Deployment \n",
|
||
"| \n",
|
||
"243\n",
|
||
"\n",
|
||
"A more advanced deployment model is based on containers. A container isolates an\n",
|
||
"application in an image of the application and its environment. A container image\n",
|
||
"includes the application plus all the dependencies it needs to run. A container plat‐\n",
|
||
"form, such as Docker, can then install and execute a pregenerated container image on\n",
|
||
"any system in which it runs.\n",
|
||
"Another deployment option, formally known as Platform as a Service (PaaS), frees\n",
|
||
"the application developer from the mundane tasks of installing and maintaining the\n",
|
||
"hardware and software platforms on which the application runs. In the PaaS model, a\n",
|
||
"service provider offers a fully managed platform on which applications can run. All\n",
|
||
"the application developer needs to do is upload the application code to the servers\n",
|
||
"maintained by the provider, after which it automatically becomes available, usually\n",
|
||
"within seconds. Most PaaS providers offer ways to dynamically “scale” the application\n",
|
||
"by adding or removing servers as necessary to keep up with the number of requests\n",
|
||
"received.\n",
|
||
"The remainder of this chapter offers an introduction to Heroku (one of the most pop‐\n",
|
||
"ular PaaS providers), Docker containers, and finally traditional deployments, which\n",
|
||
"are suitable for dedicated or virtual servers.\n",
|
||
"The Heroku Platform\n",
|
||
"Heroku was one of the first PaaS providers, having been in business since 2007. The\n",
|
||
"Heroku platform is very flexible and supports a long list of programming languages,\n",
|
||
"including Python. To deploy an application to Heroku, the developer uses Git to push\n",
|
||
"the application to Heroku’s special Git server, which automatically triggers the instal‐\n",
|
||
"lation, upgrade, configuration, and deployment of the application.\n",
|
||
"Heroku uses units of computing called dynos to measure usage and charge for the\n",
|
||
"service. The most common type of dyno is the web dyno, which represents a web\n",
|
||
"server instance. An application can increase its request handling capacity by deploy‐\n",
|
||
"ing more web dynos, each running an instance of the application. Another type of\n",
|
||
"dyno is the worker dyno, which is used to perform background jobs or other support\n",
|
||
"tasks.\n",
|
||
"The platform provides a large number of plug-ins and add-ons for databases, email\n",
|
||
"support, and many other services. The following sections expand on some of the\n",
|
||
"details involved in deploying Flasky to Heroku.\n",
|
||
"Preparing the Application\n",
|
||
"To work with Heroku, the application must be hosted in a Git repository. If you are\n",
|
||
"working with an application that is hosted on a remote Git server, such as GitHub or\n",
|
||
"Bitbucket, cloning the application will create a local Git repository that is perfect to\n",
|
||
"244 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"use with Heroku. If the application isn’t already hosted in a Git repository, you’ll need\n",
|
||
"to create one for it on your development machine.\n",
|
||
"If you plan on hosting your application on Heroku, it is a good idea\n",
|
||
"to start using Git from the very beginning. GitHub has installation\n",
|
||
"and setup guides for the three major operating systems in its help\n",
|
||
"guide.\n",
|
||
"Creating a Heroku account\n",
|
||
"You must create an account with Heroku before you can use the service. Heroku pro‐\n",
|
||
"vides a free tier that allows you to host a few simple applications, so this is a great\n",
|
||
"platform to experiment with.\n",
|
||
"Installing the Heroku CLI\n",
|
||
"To work with the Heroku service, the Heroku CLI must be installed. This is a\n",
|
||
"command-line client that manages the interactions with the service. Heroku provides\n",
|
||
"installers for the three major operating systems.\n",
|
||
"The first thing to do after installing the CLI is to authenticate with your Heroku\n",
|
||
"account through the heroku login command:\n",
|
||
"$ heroku login\n",
|
||
"Enter your Heroku credentials.\n",
|
||
"Email: <your-email-address>\n",
|
||
"Password: <your-password>\n",
|
||
"It is important that your SSH public key is uploaded to Heroku, as\n",
|
||
"this is what enables the git push command. Normally the login\n",
|
||
"command creates and uploads an SSH public key automatically,\n",
|
||
"but the heroku keys:add command can be used to upload your\n",
|
||
"public key separately from the login command or if you need to\n",
|
||
"upload additional keys.\n",
|
||
"Creating an application\n",
|
||
"The next step is to create an application. Before this is done, the application needs to\n",
|
||
"be under Git source control. If you have been using the GitHub repository to follow\n",
|
||
"along with the code in this book, then you already have a Git repository. If not, you\n",
|
||
"will need to create one now. To register the application with Heroku, run the follow‐\n",
|
||
"ing command from the application’s top-level directory:\n",
|
||
"$ heroku create <appname>\n",
|
||
"Creating <appname>... done\n",
|
||
"https://<appname>.herokuapp.com/ | https://git.heroku.com/<appname>.git\n",
|
||
"The Heroku Platform \n",
|
||
"| \n",
|
||
"245\n",
|
||
"\n",
|
||
"Heroku application names must be unique across all customers, so you need to think\n",
|
||
"of a name that is not taken by any other application. As indicated by the output of the\n",
|
||
"create command, once deployed the application will be available at https://<app‐\n",
|
||
"name>.herokuapp.com. Heroku also supports using a custom domain name for your\n",
|
||
"application.\n",
|
||
"As part of the application creation, Heroku creates a Git server dedicated to your\n",
|
||
"application at https://git.heroku.com/<appname>.git. The create command adds this\n",
|
||
"server to your local Git repository as a git remote with the name heroku:\n",
|
||
"$ git remote show heroku\n",
|
||
"* remote heroku\n",
|
||
" Fetch URL: https://git.heroku.com/<appname>.git\n",
|
||
" Push URL: https://git.heroku.com/<appname>.git\n",
|
||
" HEAD branch: (unknown)\n",
|
||
"The flask command requires the FLASK_APP environment variable to be set to work.\n",
|
||
"To make sure that any commands that are executed in the Heroku environment suc‐\n",
|
||
"ceed, it is a good idea to register this environment variable so that it is always set\n",
|
||
"when Heroku executes commands related to this application. This can be done with\n",
|
||
"the config command:\n",
|
||
"$ heroku config:set FLASK_APP=flasky.py\n",
|
||
"Setting FLASK_APP and restarting <appname>... done, v4\n",
|
||
"FLASK_APP: flasky.py\n",
|
||
"Provisioning a database\n",
|
||
"Heroku supports Postgres databases as an add-on. The free service tier includes a\n",
|
||
"small database of up to 10,000 rows. To attach a Postgres database to your application,\n",
|
||
"use the following command:\n",
|
||
"$ heroku addons:create heroku-postgresql:hobby-dev\n",
|
||
"Creating heroku-postgresql:hobby-dev on <appname>... free\n",
|
||
"Database has been created and is available\n",
|
||
" ! This database is empty. If upgrading, you can transfer\n",
|
||
" ! data from another database with pg:copy\n",
|
||
"Created postgresql-cubic-41298 as DATABASE_URL\n",
|
||
"Use heroku addons:docs heroku-postgresql to view documentation\n",
|
||
"As indicated by the output of the command, once the application runs inside the Her‐\n",
|
||
"oku platform, it will see the database location and credentials in the DATABASE_URL\n",
|
||
"environment variable. The format of this variable is a URL, exactly in the format\n",
|
||
"SQLAlchemy expects. Recall that the config.py script uses the value of DATABASE_URL\n",
|
||
"if it is defined, so the connection to the Postgres database will work automatically.\n",
|
||
"246 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"Configuring logging\n",
|
||
"Logging of fatal errors by email was added earlier, but in addition to that it is impor‐\n",
|
||
"tant to configure logging of lesser message categories. A good example of these types\n",
|
||
"of messages are the warnings for slow database queries added in Chapter 16.\n",
|
||
"Heroku considers any output written by the application to stdout or stderr logs, so\n",
|
||
"a logging handler needs to be added to generate this output. The logging output is\n",
|
||
"captured by Heroku and made accessible through the Heroku client with the heroku\n",
|
||
"logs command.\n",
|
||
"The logging configuration can be added to the ProductionConfig class in its\n",
|
||
"init_app() static method. But since this type of logging is specific to Heroku, a bet‐\n",
|
||
"ter approach is to define a new configuration specifically for this platform, leaving\n",
|
||
"ProductionConfig as a baseline configuration for different types of production plat‐\n",
|
||
"forms. The HerokuConfig class is shown in Example 17-3.\n",
|
||
"Example 17-3. config.py: Heroku configuration\n",
|
||
"class HerokuConfig(ProductionConfig):\n",
|
||
" @classmethod\n",
|
||
" def init_app(cls, app):\n",
|
||
" ProductionConfig.init_app(app)\n",
|
||
" # log to stderr\n",
|
||
" import logging\n",
|
||
" from logging import StreamHandler\n",
|
||
" file_handler = StreamHandler()\n",
|
||
" file_handler.setLevel(logging.INFO)\n",
|
||
" app.logger.addHandler(file_handler)\n",
|
||
"When the application is executed by Heroku, it needs to know that this new configu‐\n",
|
||
"ration needs to be used. The application instance created in flasky.py uses the\n",
|
||
"FLASK_CONFIG environment variable to know what configuration to use, so this vari‐\n",
|
||
"able needs to be set appropriately in the Heroku environment. Environment variables\n",
|
||
"for the Heroku environment are set using the Heroku client’s config:set command:\n",
|
||
"$ heroku config:set FLASK_CONFIG=heroku\n",
|
||
"Setting FLASK_CONFIG and restarting <appname>... done, v4\n",
|
||
"FLASK_CONFIG: heroku\n",
|
||
"To increase the security of your application, it is a good idea to configure a difficult-\n",
|
||
"to-guess string as the application’s secret key, which is used to sign the user session\n",
|
||
"and the authentication tokens. The Config base class includes the SECRET_KEY\n",
|
||
"attribute for this purpose, and sets its value from an environment variable of the same\n",
|
||
"name if it exists. When working on the application in your development system it is\n",
|
||
"okay to leave this variable undefined and let the Config class configure a hardcoded\n",
|
||
"The Heroku Platform \n",
|
||
"| \n",
|
||
"247\n",
|
||
"\n",
|
||
"value, but on a production platform it is extremely important to set a strong secret\n",
|
||
"key that is not known to anyone, since a leaked key will enable an attacker to forge\n",
|
||
"the contents of the user session or generate valid tokens. To make your key secure,\n",
|
||
"just set the SECRET_KEY environment variable to a unique string that is not stored\n",
|
||
"anywhere:\n",
|
||
"$ heroku config:set SECRET_KEY=d68653675379485599f7876a3b469a57\n",
|
||
"Setting SECRET_KEY and restarting <appname>... done, v4\n",
|
||
"SECRET_KEY: d68653675379485599f7876a3b469a57\n",
|
||
"There are many ways to generate random strings that are appropriate to be used as\n",
|
||
"secret keys. You can do so with Python as follows:\n",
|
||
"(venv) $ python -c \"import uuid; print(uuid.uuid4().hex)\"\n",
|
||
"d68653675379485599f7876a3b469a57\n",
|
||
"Configuring email\n",
|
||
"Heroku does not provide an SMTP server, so an external server must be configured.\n",
|
||
"There are several third-party add-ons that integrate production-ready email sending\n",
|
||
"support with Heroku, but for testing and evaluation purposes it is sufficient to use the\n",
|
||
"default Gmail configuration inherited from the base Config class.\n",
|
||
"Because it can be a security risk to embed login credentials directly in the script, the\n",
|
||
"username and password to access the Gmail SMTP server are provided as environ‐\n",
|
||
"ment variables (if you haven’t yet, it is a very good idea that instead of using your per‐\n",
|
||
"sonal email account you create a secondary email to use for testing):\n",
|
||
"$ heroku config:set MAIL_USERNAME=<your-gmail-username>\n",
|
||
"$ heroku config:set MAIL_PASSWORD=<your-gmail-password>\n",
|
||
"Adding a top-level requirements file\n",
|
||
"Heroku installs package dependencies from a requirements.txt file stored in the top-\n",
|
||
"level directory of the application. All the dependencies in this file will be imported\n",
|
||
"into a virtual environment managed by Heroku as part of the deployment.\n",
|
||
"The Heroku requirements file must include all the common requirements for the\n",
|
||
"production version of the application, plus the psycopg2 package that enables SQL‐\n",
|
||
"Alchemy to access the Postgres database. A heroku.txt file with these dependencies\n",
|
||
"can be added in the requirements directory and then imported from the top-level\n",
|
||
"requirements.txt file as shown in Example 17-4.\n",
|
||
"Example 17-4. requirements.txt: Heroku requirements file\n",
|
||
"-r requirements/heroku.txt\n",
|
||
"248 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"Enabling Secure HTTP with Flask-SSLify\n",
|
||
"When the user logs in to the application by submitting a username and a password in\n",
|
||
"a web form, these values are at risk of being intercepted by a malicious third party, as\n",
|
||
"discussed several times before. During development this is not a problem, but this\n",
|
||
"risk needs to be eliminated when you deploy the application on a production server.\n",
|
||
"To prevent user credentials from being exposed while in transit, it is necessary to use\n",
|
||
"secure HTTP, which encrypts all the communications between clients and the server\n",
|
||
"using public key cryptography.\n",
|
||
"Heroku makes all applications that are accessed on the herokuapp.com domain avail‐\n",
|
||
"able on both http:// and https:// without any configuration required. Because the\n",
|
||
"application runs on Heroku’s domain, it will use Heroku’s own SSL certificate. The\n",
|
||
"only necessary action to fully secure the application is to intercept any requests sent\n",
|
||
"to the http:// interface and redirect them to https://, which is exactly what the Flask-\n",
|
||
"SSLify extension does.\n",
|
||
"As usual, Flask-SSLify is installed with pip:\n",
|
||
"(venv) $ pip install flask-sslify\n",
|
||
"The code that activates this extension is added to the application factory function, as\n",
|
||
"shown in Example 17-5.\n",
|
||
"Example 17-5. app/__init__.py: redirecting all requests to secure HTTP\n",
|
||
"def create_app(config_name):\n",
|
||
" # ...\n",
|
||
" if app.config['SSL_REDIRECT']:\n",
|
||
" from flask_sslify import SSLify\n",
|
||
" sslify = SSLify(app)\n",
|
||
" # ...\n",
|
||
"Support for SSL needs to be enabled only in production mode, and only when the\n",
|
||
"platform supports it. To make it easy to switch SSL on and off, a new configuration\n",
|
||
"variable called SSL_REDIRECT is added. The base Config class sets it to False, so that\n",
|
||
"SSL redirects are not used by default, and the class HerokuConfig overrides it so that\n",
|
||
"only on that configuration are the redirects issued. The implementation of this con‐\n",
|
||
"figuration variable is shown in Example 17-6.\n",
|
||
"The Heroku Platform \n",
|
||
"| \n",
|
||
"249\n",
|
||
"\n",
|
||
"Example 17-6. config.py: configuring the use of SSL\n",
|
||
"class Config:\n",
|
||
" # ...\n",
|
||
" SSL_REDIRECT = False\n",
|
||
"class HerokuConfig(ProductionConfig):\n",
|
||
" # ...\n",
|
||
" SSL_REDIRECT = True if os.environ.get('DYNO') else False\n",
|
||
"The value of SSL_REDIRECT in HerokuConfig is only set to True if the environment\n",
|
||
"variable DYNO exists. This variable is set by Heroku in its environment, so using the\n",
|
||
"Heroku configuration for local testing does not activate the SSL redirects.\n",
|
||
"With these changes, the users will be forced to use the SSL server when accessing the\n",
|
||
"application on Heroku—but there is one more detail that needs to be handled to\n",
|
||
"make this support complete. When using Heroku, clients do not connect to the appli‐\n",
|
||
"cation directly but to a reverse proxy server. The reverse proxy server receives requests\n",
|
||
"from many applications, and forwards them to each of them as appropriate. In this\n",
|
||
"type of setup, only the proxy server runs in SSL mode; the SSL connection is termi‐\n",
|
||
"nated at the proxy server, and applications receive the forwarded requests from the\n",
|
||
"proxy server without encryption. This presents a problem when the application needs\n",
|
||
"to generate absolute URLs, because in the Flask application the request object\n",
|
||
"describes the forwarded request, which is not encrypted, and not the original request\n",
|
||
"sent by the client through an encrypted connection.\n",
|
||
"An example of the problem this can cause is with the generation of account confirma‐\n",
|
||
"tion or password reset links that are sent by email to users. When url_for() is called\n",
|
||
"with _external=True to generate an absolute URL for these links, Flask will use\n",
|
||
"http:// for them, because it does not know that there is a reverse proxy that is accept‐\n",
|
||
"ing encrypted connections from the outside.\n",
|
||
"Proxy servers pass information that describes the original request from the client to\n",
|
||
"the redirected web servers through custom HTTP headers, so it is possible to deter‐\n",
|
||
"mine whether the user is communicating with the application over SSL by looking at\n",
|
||
"these headers. Werkzeug provides a WSGI middleware that checks the custom head‐\n",
|
||
"ers from the proxy server and updates the request object accordingly so that, for\n",
|
||
"example, request.is_secure reflects the encryption state of the request that the cli‐\n",
|
||
"ent sent to the reverse proxy server and not the request that the proxy server then\n",
|
||
"forwarded to the application. Example 17-7 shows how to add the ProxyFix middle‐\n",
|
||
"ware to the application.\n",
|
||
"250 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"Example 17-7. config.py: adding support for proxy servers\n",
|
||
"class HerokuConfig(ProductionConfig):\n",
|
||
" # ...\n",
|
||
" @classmethod\n",
|
||
" def init_app(cls, app):\n",
|
||
" # ...\n",
|
||
" # handle reverse proxy server headers\n",
|
||
" from werkzeug.contrib.fixers import ProxyFix\n",
|
||
" app.wsgi_app = ProxyFix(app.wsgi_app)\n",
|
||
"The middleware is added in the initialization method for the Heroku configuration.\n",
|
||
"WSGI middlewares such as ProxyFix are added by wrapping the WSGI application.\n",
|
||
"When a request comes, the middlewares get a chance to inspect the environment and\n",
|
||
"make changes before the request is processed. The ProxyFix middleware is necessary\n",
|
||
"not only for Heroku but in any deployment that uses a reverse proxy server.\n",
|
||
"Running a production web server\n",
|
||
"Heroku expects applications to start their own production web server and configure\n",
|
||
"it to listen to requests on the port number set in the environment variable PORT.\n",
|
||
"The development web server that comes with Flask will perform very poorly in this\n",
|
||
"situation because it is not designed to run in a production environment. Two \n",
|
||
"production-ready web servers that work well with Flask applications are Gunicorn\n",
|
||
"and uWSGI.\n",
|
||
"It is a good idea to install the chosen web server in the local virtual environment, so\n",
|
||
"that it can be tested in a way similar to how it will run in the Heroku environment.\n",
|
||
"For example, Gunicorn is installed as follows:\n",
|
||
"(venv) $ pip install gunicorn\n",
|
||
"To run the application locally under Gunicorn, use the following command:\n",
|
||
"(venv) $ gunicorn flasky:app\n",
|
||
"[2017-08-03 23:54:36 -0700] [INFO] Starting gunicorn 19.7.1\n",
|
||
"[2017-08-03 23:54:36 -0700] [INFO] Listening at: http://127.0.0.1:8000 (68982)\n",
|
||
"[2017-08-03 23:54:36 -0700] [INFO] Using worker: sync\n",
|
||
"[2017-08-03 23:54:36 -0700] [INFO] Booting worker with pid: 68985\n",
|
||
"The flasky:app argument tells Gunicorn where the application instance is located.\n",
|
||
"The name given before the colon is the package or module that defines this instance,\n",
|
||
"while the name after the colon is the actual application instance name. Note that\n",
|
||
"Gunicorn uses port 8000 by default, not 5000 like Flask. Like the Flask development\n",
|
||
"web server, you can exit Gunicorn with Ctrl+C.\n",
|
||
"The Heroku Platform \n",
|
||
"| \n",
|
||
"251\n",
|
||
"\n",
|
||
"The Gunicorn web server does not work on Microsoft Windows.\n",
|
||
"The other recommended web server, uWSGI, does work on Win‐\n",
|
||
"dows, but it can be difficult to install due to it being written in\n",
|
||
"native code. If you want to test the Heroku deployment on your\n",
|
||
"Windows system, you can use Waitress, which is another pure\n",
|
||
"Python web server that is in many ways similar to Gunicorn but\n",
|
||
"has the advantage that it fully supports Windows. Waitress is\n",
|
||
"installed with pip:\n",
|
||
"(venv) $ pip install waitress\n",
|
||
"To start the Waitress web server, use the waitress-serve com‐\n",
|
||
"mand:\n",
|
||
"(venv) $ waitress-serve --port 8000 flasky:app\n",
|
||
"Adding a Procfile\n",
|
||
"Heroku needs to know what command to use to start the application. This command\n",
|
||
"is given in a special file called Procfile. This file must be included in the top-level\n",
|
||
"directory of the application.\n",
|
||
"Example 17-8 shows the contents of this file.\n",
|
||
"Example 17-8. Procfile: Heroku Procfile\n",
|
||
"web: gunicorn flasky:app\n",
|
||
"The format for the Procfile is very simple: in each line a task name is given, followed\n",
|
||
"by a colon and then the command that runs the task. The task name web is special; it\n",
|
||
"is recognized by Heroku as the task that starts the web server. Heroku will give this\n",
|
||
"task a PORT environment variable set to the port on which the application needs to\n",
|
||
"listen for requests. Gunicorn by default honors the PORT variable if it is set in the\n",
|
||
"environment, so there is no need to include it in the startup command.\n",
|
||
"If you are using Microsoft Windows, or need your application to be\n",
|
||
"fully compatible with that platform, you can instead use the Wait‐\n",
|
||
"ress web server:\n",
|
||
"web: waitress-serve --port=$PORT flasky:app\n",
|
||
"Applications can declare additional tasks with names other than\n",
|
||
"web in the Procfile. Each task included in the Procfile will be started\n",
|
||
"on its own dyno.\n",
|
||
"252 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 17c to check out this version of the applica‐\n",
|
||
"tion. If you are using Microsoft Windows, run git checkout 17c-\n",
|
||
"waitress to check out a version of the application configured to\n",
|
||
"use the Waitress web server instead of Gunicorn.\n",
|
||
"Testing with Heroku Local\n",
|
||
"The Heroku CLI includes the local command, used to run the application locally in\n",
|
||
"a very similar way to how it runs on the Heroku servers. However, environment vari‐\n",
|
||
"ables such as FLASK_APP are not available when running the application locally. The\n",
|
||
"heroku local command looks for environment variables that configure the applica‐\n",
|
||
"tion in a file named .env in the top-level directory of the application. For example,\n",
|
||
"the .env file can contain the following variables:\n",
|
||
"FLASK_APP=flasky.py\n",
|
||
"FLASK_CONFIG=heroku\n",
|
||
"MAIL_USERNAME=<your-gmail-username>\n",
|
||
"MAIL_PASSWORD=<your-gmail-password>\n",
|
||
"Because the .env file contains passwords and other sensitive\n",
|
||
"account information, it should never be added to source control.\n",
|
||
"Before the application can be started, the deployment task needs to be executed to set\n",
|
||
"up the database. One-off tasks can be executed with the local:run command:\n",
|
||
"(venv) $ heroku local:run flask deploy\n",
|
||
"[OKAY] Loaded ENV .env File as KEY=VALUE Format\n",
|
||
"INFO Context impl SQLiteImpl.\n",
|
||
"INFO Will assume non-transactional DDL.\n",
|
||
"INFO Running upgrade -> 38c4e85512a9, initial migration\n",
|
||
"INFO Running upgrade 38c4e85512a9 -> 456a945560f6, login support\n",
|
||
"INFO Running upgrade 456a945560f6 -> 190163627111, account confirmation\n",
|
||
"INFO Running upgrade 190163627111 -> 56ed7d33de8d, user roles\n",
|
||
"INFO Running upgrade 56ed7d33de8d -> d66f086b258, user information\n",
|
||
"INFO Running upgrade d66f086b258 -> 198b0eebcf9, caching of avatar hashes\n",
|
||
"INFO Running upgrade 198b0eebcf9 -> 1b966e7f4b9e, post model\n",
|
||
"INFO Running upgrade 1b966e7f4b9e -> 288cd3dc5a8, rich text posts\n",
|
||
"INFO Running upgrade 288cd3dc5a8 -> 2356a38169ea, followers\n",
|
||
"INFO Running upgrade 2356a38169ea -> 51f5ccfba190, comments\n",
|
||
"The Heroku Platform \n",
|
||
"| \n",
|
||
"253\n",
|
||
"\n",
|
||
"The heroku local command reads the Procfile and executes the tasks defined by it:\n",
|
||
"(venv) $ heroku local\n",
|
||
"[OKAY] Loaded ENV .env File as KEY=VALUE Format\n",
|
||
"11:37:49 AM web.1 | [INFO] Starting gunicorn 19.7.1\n",
|
||
"11:37:49 AM web.1 | [INFO] Listening at: http://0.0.0.0:5000 (91686)\n",
|
||
"11:37:49 AM web.1 | [INFO] Using worker: sync\n",
|
||
"11:37:49 AM web.1 | [INFO] Booting worker with pid: 91689\n",
|
||
"The logging output of all the tasks started by this command is consolidated into a sin‐\n",
|
||
"gle stream that is printed to the console, with each line prefixed with a timestamp and\n",
|
||
"the task name.\n",
|
||
"The heroku local command also allows simulation of the use of multiple dynos to\n",
|
||
"scale the application. The following command starts three web workers, each listen‐\n",
|
||
"ing on a different port:\n",
|
||
"(venv) $ heroku local web=3\n",
|
||
"Deploying with git push\n",
|
||
"The final step in the process is to upload the application to the Heroku servers. Make\n",
|
||
"sure that all the changes are committed to the local Git repository and then use git\n",
|
||
"push heroku master to upload the application to the heroku remote:\n",
|
||
"$ git push heroku master\n",
|
||
"Counting objects: 502, done.\n",
|
||
"Delta compression using up to 8 threads.\n",
|
||
"Compressing objects: 100% (426/426), done.\n",
|
||
"Writing objects: 100% (502/502), 108.03 KiB | 0 bytes/s, done.\n",
|
||
"Total 502 (delta 303), reused 146 (delta 61)\n",
|
||
"remote: Compressing source files... done.\n",
|
||
"remote: Building source:\n",
|
||
"remote:\n",
|
||
"remote: -----> Python app detected\n",
|
||
"remote: -----> Installing python-3.6.2\n",
|
||
"remote: -----> Installing pip\n",
|
||
"remote: -----> Installing requirements with pip\n",
|
||
"...\n",
|
||
"remote: -----> Discovering process types\n",
|
||
"remote: Procfile declares types -> web\n",
|
||
"remote:\n",
|
||
"remote: -----> Compressing...\n",
|
||
"remote: Done: 49.4M\n",
|
||
"remote: -----> Launching...\n",
|
||
"remote: Released v8\n",
|
||
"remote: https://<appname>.herokuapp.com/ deployed to Heroku\n",
|
||
"remote:\n",
|
||
"remote: Verifying deploy... done.\n",
|
||
"To https://git.heroku.com/<appname>.git\n",
|
||
" * [new branch] master -> master\n",
|
||
"254 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"The application is now deployed and running, but it is not going to work correctly\n",
|
||
"because the deploy command that initializes the database tables has not been exe‐\n",
|
||
"cuted yet. The Heroku client can run this command as follows:\n",
|
||
"$ heroku run flask deploy\n",
|
||
"Running flask deploy on <appname>... up, run.3771 (Free)\n",
|
||
"INFO [alembic.runtime.migration] Context impl PostgresqlImpl.\n",
|
||
"INFO [alembic.runtime.migration] Will assume transactional DDL.\n",
|
||
"...\n",
|
||
"After the database tables are created and configured, the application can be restarted\n",
|
||
"so that it starts cleanly with an updated database:\n",
|
||
"$ heroku restart\n",
|
||
"Restarting dynos on <appname>... done\n",
|
||
"The application should now be fully deployed and online at https://<appname>.hero‐\n",
|
||
"kuapp.com.\n",
|
||
"Reviewing application logs\n",
|
||
"The logging output generated by the application is captured by Heroku. To view the\n",
|
||
"contents of the log, use the logs command:\n",
|
||
"$ heroku logs\n",
|
||
"During testing it can also be convenient to “tail” the log file, which can be done as\n",
|
||
"follows:\n",
|
||
"$ heroku logs -t\n",
|
||
"Deploying an Upgrade\n",
|
||
"When a Heroku application needs to be upgraded the same process needs to be\n",
|
||
"repeated. After all the changes have been committed to the Git repository, the follow‐\n",
|
||
"ing commands perform an upgrade:\n",
|
||
"$ heroku maintenance:on\n",
|
||
"$ git push heroku master\n",
|
||
"$ heroku run flask deploy\n",
|
||
"$ heroku restart\n",
|
||
"$ heroku maintenance:off\n",
|
||
"The maintenance option available on the Heroku CLI will take the application offline\n",
|
||
"during the upgrade and will show a static page that informs users that the site will be\n",
|
||
"coming back soon. This prevents users from accessing the application while it is\n",
|
||
"going through the upgrade process.\n",
|
||
"The Heroku Platform \n",
|
||
"| \n",
|
||
"255\n",
|
||
"\n",
|
||
"Docker Containers\n",
|
||
"You are now familiar with Heroku, which is a fairly high-level deployment option. In\n",
|
||
"this section you will learn how to work with containers, and in particular with the\n",
|
||
"Docker platform, which is not as automated as a PaaS but provides more flexibility\n",
|
||
"and is not tied to a specific cloud provider.\n",
|
||
"Containers are a special type of virtual machine that run on top of the kernel of the\n",
|
||
"host operating system, unlike standard virtual machines, which have their own vir‐\n",
|
||
"tualized kernel and hardware. Because the virtualization stops at the kernel, contain‐\n",
|
||
"ers are much more lightweight and efficient than virtual machines, but they require\n",
|
||
"dedicated support built into the operating system. The Linux kernel has full support\n",
|
||
"for containers.\n",
|
||
"Installing Docker\n",
|
||
"The most popular container platform is Docker, which has a free Community Edition\n",
|
||
"(known as Docker CE) and a subscription-based Enterprise Edition (Docker EE).\n",
|
||
"Docker can be installed on the three major desktop operating systems, and also on\n",
|
||
"cloud servers. The easiest way to develop and test a “containerized” application is to\n",
|
||
"install Docker CE on your development system. For macOS and Microsoft Windows\n",
|
||
"there are one-click installers available from the Docker Store. This page also includes\n",
|
||
"installation instructions for CentOS, Fedora, Debian, and Ubuntu Linux distribu‐\n",
|
||
"tions.\n",
|
||
"After you complete the installation of Docker CE on your system, you should be able\n",
|
||
"to access the docker command from your terminal:\n",
|
||
"$ docker version\n",
|
||
"Client:\n",
|
||
" Version: 17.06.0-ce\n",
|
||
" API version: 1.30\n",
|
||
" Go version: go1.8.3\n",
|
||
" Git commit: 02c1d87\n",
|
||
" Built: Fri Jun 23 21:31:53 2017\n",
|
||
" OS/Arch: darwin/amd64\n",
|
||
"Server:\n",
|
||
" Version: 17.06.0-ce\n",
|
||
" API version: 1.30 (minimum version 1.12)\n",
|
||
" Go version: go1.8.3\n",
|
||
" Git commit: 02c1d87\n",
|
||
" Built: Fri Jun 23 21:51:55 2017\n",
|
||
" OS/Arch: linux/amd64\n",
|
||
" Experimental: true\n",
|
||
"256 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"Docker for Windows requires Microsoft’s Hyper-V feature to be\n",
|
||
"enabled. The installer will normally enable it for you, but if Docker\n",
|
||
"does not appear to work correctly after installation, the state of the\n",
|
||
"Hyper-V hypervisor is the first thing to check. You should keep in\n",
|
||
"mind that enabling Hyper-V on your Windows machine will pre‐\n",
|
||
"vent other hypervisors (such as Oracle’s VirtualBox) from working.\n",
|
||
"If your system does not support Hyper-V virtualization, or you\n",
|
||
"need a Docker solution that does not render other virtualization\n",
|
||
"technologies unusable, you may want to install Docker Toolbox, a\n",
|
||
"legacy Docker product for Windows that is based on VirtualBox.\n",
|
||
"Building a Container Image\n",
|
||
"The first task when working with containers is to build a container image for the\n",
|
||
"application. An image is a snapshot of a container’s filesystem, used as a template\n",
|
||
"when starting new containers. Docker expects the instructions to create the image to\n",
|
||
"be provided in a file named Dockerfile. Example 17-9 shows a Dockerfile that builds\n",
|
||
"the application featured in this book.\n",
|
||
"Example 17-9. Dockerfile: container image build script\n",
|
||
"FROM python:3.6-alpine\n",
|
||
"ENV FLASK_APP flasky.py\n",
|
||
"ENV FLASK_CONFIG docker\n",
|
||
"RUN adduser -D flasky\n",
|
||
"USER flasky\n",
|
||
"WORKDIR /home/flasky\n",
|
||
"COPY requirements requirements\n",
|
||
"RUN python -m venv venv\n",
|
||
"RUN venv/bin/pip install -r requirements/docker.txt\n",
|
||
"COPY app app\n",
|
||
"COPY migrations migrations\n",
|
||
"COPY flasky.py config.py boot.sh ./\n",
|
||
"# runtime configuration\n",
|
||
"EXPOSE 5000\n",
|
||
"ENTRYPOINT [\"./boot.sh\"]\n",
|
||
"The build commands that can be included in a Dockerfile are documented in detail in\n",
|
||
"the Dockerfile reference. In essence, these are deployment commands that install and\n",
|
||
"configure the application in the container’s filesystem, which is isolated from your\n",
|
||
"system.\n",
|
||
"Docker Containers \n",
|
||
"| \n",
|
||
"257\n",
|
||
"\n",
|
||
"The FROM command is required in all Dockerfiles to specify a base container image to\n",
|
||
"start from. In most cases, this is going to be an image that is publicly available in\n",
|
||
"Docker Hub, Docker’s container image repository. The repository contains official\n",
|
||
"images for several Python interpreter versions. These are images that have a base\n",
|
||
"operating system with Python installed on it. Images are specified with a name and a\n",
|
||
"tag. The name of the official Docker Hub Python image is simply python. The differ‐\n",
|
||
"ent tags that are available can be seen in the Docker Hub page for the image. For the\n",
|
||
"python image, tags are used to specify the desired interpreter version and platform.\n",
|
||
"For this application, a 3.6 interpreter built on top of the Alpine Linux distribution is\n",
|
||
"used. Alpine Linux is a platform commonly used in container images due to its small\n",
|
||
"size.\n",
|
||
"The macOS and Windows versions of Docker are able to run\n",
|
||
"Linux-based containers.\n",
|
||
"The ENV command defines runtime environment variables. This command takes two\n",
|
||
"arguments: a variable name and its value. Any environment variables defined with\n",
|
||
"this command will be available when a container based on this image is executed. The\n",
|
||
"FLASK_APP variable required by the flask command is defined here, as is\n",
|
||
"FLASK_CONFIG, which is the name of the configuration class the application uses to\n",
|
||
"configure itself when it starts. The Docker deployment will use a new configuration\n",
|
||
"called docker, implemented in a DockerConfig class as shown in Example 17-10. This\n",
|
||
"new configuration class inherits from ProductionConfig and just configures logging\n",
|
||
"to be directed to stderr, which Docker automatically captures and exposes through\n",
|
||
"the docker logs command.\n",
|
||
"Example 17-10. config.py: Docker configuration\n",
|
||
"class DockerConfig(ProductionConfig):\n",
|
||
" @classmethod\n",
|
||
" def init_app(cls, app):\n",
|
||
" ProductionConfig.init_app(app)\n",
|
||
" # log to stderr\n",
|
||
" import logging\n",
|
||
" from logging import StreamHandler\n",
|
||
" file_handler = StreamHandler()\n",
|
||
" file_handler.setLevel(logging.INFO)\n",
|
||
" app.logger.addHandler(file_handler)\n",
|
||
"config = {\n",
|
||
" # ...\n",
|
||
"258 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
" 'docker': DockerConfig,\n",
|
||
" # ...\n",
|
||
"}\n",
|
||
"The RUN command executes a command in the context of the container image. In the\n",
|
||
"first occurrence of RUN, a flasky user is created inside the container. The adduser com‐\n",
|
||
"mand is part of Alpine Linux, and is available in the base image selected by the FROM\n",
|
||
"command. The -D argument to adduser suppresses an interactive prompt for the\n",
|
||
"user’s password.\n",
|
||
"The USER command selects the user under which the container will run, and also the\n",
|
||
"user for the remaining commands in the Dockerfile. Docker uses the root user by\n",
|
||
"default, but it is considered a good practice to switch to a regular user when root\n",
|
||
"access isn’t needed.\n",
|
||
"The WORKDIR command defines the top-level directory where the application is going\n",
|
||
"to be installed. For this application, the home directory for the newly created flasky\n",
|
||
"user is used. The remaining commands in the Dockerfile will execute with this direc‐\n",
|
||
"tory as the current directory.\n",
|
||
"The COPY command copies files from the local filesystem to the container’s filesystem.\n",
|
||
"The requirements, app, and migrations directories are copied in their entirety, and\n",
|
||
"then the top-level flasky.py, config.py, and new boot.sh files (discussed shortly) are\n",
|
||
"copied as well.\n",
|
||
"The two additional RUN commands create a virtual environment and install the\n",
|
||
"requirements in it. A dedicated requirements file was created for Docker as require‐\n",
|
||
"ments/docker.txt. This file imports all the dependencies from requirements/\n",
|
||
"common.txt and adds Gunicorn, which will be used as a web server as in the Heroku\n",
|
||
"deployment.\n",
|
||
"The EXPOSE command defines the port on which the application running inside the\n",
|
||
"container will install its server. When the container is started, Docker will map this\n",
|
||
"port to a real port on the host machine, so that the container can receive requests\n",
|
||
"from the outside world.\n",
|
||
"The final command is ENTRYPOINT. This command specifies how to execute the appli‐\n",
|
||
"cation when the container is started. The new boot.sh file, copied into the container\n",
|
||
"above, is used as the startup script. Example 17-11 shows the contents of this file.\n",
|
||
"Example 17-11. boot.sh: container startup script\n",
|
||
"#!/bin/sh\n",
|
||
"source venv/bin/activate\n",
|
||
"flask deploy\n",
|
||
"exec gunicorn -b 0.0.0.0:5000 --access-logfile - --error-logfile - flasky:app\n",
|
||
"Docker Containers \n",
|
||
"| \n",
|
||
"259\n",
|
||
"\n",
|
||
"The script starts by activating the venv virtual environment that was created as part of\n",
|
||
"the build. Then it runs the application’s deploy command, built earlier in this chapter\n",
|
||
"and also used for the Heroku deployment. This will create a new database, upgrade it\n",
|
||
"to the latest version, and insert the default roles. Because the DATABASE_URL environ‐\n",
|
||
"ment variable hasn’t been set, the database will use the SQLite engine. Then a Guni‐\n",
|
||
"corn server listening on port 5000 is started. Docker captures all the output from the\n",
|
||
"application and presents it as logs, so Gunicorn is configured to write both its access\n",
|
||
"and error log files to standard output. Starting Gunicorn with exec makes the Guni‐\n",
|
||
"corn process take over the process running the boot.sh file. This is done because\n",
|
||
"Docker pays special attention to the process that starts a container, and expects it to\n",
|
||
"be the main process throughout its life. When this process ends, the container ends as\n",
|
||
"well.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 17d to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"A container image for Flasky can now be built as follows:\n",
|
||
"$ docker build -t flasky:latest .\n",
|
||
"Sending build context to Docker daemon 51.08MB\n",
|
||
"Step 1/14 : FROM python:3.6-alpine\n",
|
||
" ---> a6beab4fa70b\n",
|
||
"...\n",
|
||
"Successfully built 930e17a89b42\n",
|
||
"Successfully tagged flasky:latest\n",
|
||
"The -t argument to docker build gives a name and tag to the container image, sepa‐\n",
|
||
"rated by a colon. The latest tag name is typically used for the most up-to-date ver‐\n",
|
||
"sion of a container image. The dot at the end of the build command sets the current\n",
|
||
"directory as the top-level directory during the build. Docker will look for the Docker‐\n",
|
||
"file in this directory, and will also make the files in this directory and all sub-\n",
|
||
"directories available to be added to the container image.\n",
|
||
"As a result of a successful docker build command, the built container image is\n",
|
||
"stored in a local image repository. The docker images command shows the contents\n",
|
||
"of the image repository on your system:\n",
|
||
"$ docker images\n",
|
||
"REPOSITORY TAG IMAGE ID CREATED SIZE\n",
|
||
"flasky latest 930e17a89b42 5 minutes ago 127MB\n",
|
||
"python 3.6-alpine a6beab4fa70b 3 weeks ago 88.7MB\n",
|
||
"260 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"This listing includes the just-built flasky:latest image and also the base Python 3.6\n",
|
||
"interpreter image referenced in the FROM Dockerfile, which Docker downloads and\n",
|
||
"installs as part of the build.\n",
|
||
"Running a Container\n",
|
||
"Once a container image for the application is built, all that remains is to run it. The\n",
|
||
"docker run command makes this a very simple task:\n",
|
||
"$ docker run --name flasky -d -p 8000:5000 \\\n",
|
||
" -e SECRET_KEY=57d40f677aff4d8d96df97223c74d217 \\\n",
|
||
" -e MAIL_USERNAME=<your-gmail-username> \\\n",
|
||
" -e MAIL_PASSWORD=<your-gmail-password> flasky:latest\n",
|
||
"The --name option gives the container a name. Naming containers is optional; if a\n",
|
||
"name is not given, Docker generates one using randomly selected words.\n",
|
||
"The -d option starts the container in detached mode, which means that the container\n",
|
||
"will run in the background on your system. A container that is not detached runs as a\n",
|
||
"foreground task attached to the console session.\n",
|
||
"The -p option maps port 8000 in the host system to port 5000 inside the container.\n",
|
||
"Docker provides the flexibility of mapping container ports to any port in the host sys‐\n",
|
||
"tem. This mapping enables two or more instances of the same container image to run\n",
|
||
"on different host ports, while each instance uses its own virtualized port 5000.\n",
|
||
"The -e option defines environment variables that are going to exist in the context of\n",
|
||
"the container, in addition to any variables defined at build time with the ENV com‐\n",
|
||
"mand in the Dockerfile. The value assigned to the SECRET_KEY variable ensures that\n",
|
||
"user sessions and tokens are signed with a unique and very hard to guess key. You\n",
|
||
"should generate your own unique key for this variable. The values for the\n",
|
||
"MAIL_USERNAME and MAIL_PASSWORD variables configure email sending through the\n",
|
||
"Gmail service. For a production deployment that uses a different email service pro‐\n",
|
||
"vider the MAIL_SERVER, MAIL_PORT, and MAIL_USE_TLS variables should be defined as\n",
|
||
"well.\n",
|
||
"The final argument in the docker run command is the container image and tag to\n",
|
||
"execute. This should match the name and tag given as the -t option to the docker\n",
|
||
"build command.\n",
|
||
"When the container starts in the background, the docker run command prints the\n",
|
||
"container ID to the console. This is a 256-bit unique identifier printed in hexadecimal\n",
|
||
"notation. This ID can be used in any commands that require a reference to a con‐\n",
|
||
"tainer (in practice, only the first few characters of the ID need to be provided, such\n",
|
||
"that the container can be uniquely identified).\n",
|
||
"Docker Containers \n",
|
||
"| \n",
|
||
"261\n",
|
||
"\n",
|
||
"To confirm that the container is running, the docker ps command can be used:\n",
|
||
"$ docker ps\n",
|
||
"CONTAINER ID IMAGE CREATED STATUS PORTS NAMES\n",
|
||
"71357ee776ae flasky:latest 4 secs ago Up 8 secs 0.0.0.0:8000->5000/tcp flasky\n",
|
||
"Since the container is now up and running, you can access the containerized applica‐\n",
|
||
"tion on port 8000 of your system, either locally as http://localhost:8000 or from any\n",
|
||
"other computer in the network as http://<ip-address>:8000.\n",
|
||
"To stop this container, use the docker stop command:\n",
|
||
"$ docker stop 71357ee776ae\n",
|
||
"71357ee776ae\n",
|
||
"The stop command stops the container but does not remove it from the system. To\n",
|
||
"remove it, use the docker rm command:\n",
|
||
"$ docker rm 71357ee776ae\n",
|
||
"71357ee776ae\n",
|
||
"These two operations can be combined into one with docker rm -f:\n",
|
||
"$ docker rm -f 71357ee776ae\n",
|
||
"71357ee776ae\n",
|
||
"Inspecting a Running Container\n",
|
||
"When a container appears to misbehave, it might be necessary to debug it. The most\n",
|
||
"obvious debugging mechanism is to add logging statements to the application and\n",
|
||
"then monitor the running container with the docker logs command.\n",
|
||
"In some situations, however, it might be more convenient to open a shell session on\n",
|
||
"the running container so that it can be inspected more closely. The docker exec\n",
|
||
"command makes this possible:\n",
|
||
"$ docker exec -it 71357ee776ae sh\n",
|
||
"In this example, Docker is going to open a shell session with sh (the Unix shell)\n",
|
||
"without interrupting the container. The -it options connect the terminal session\n",
|
||
"from which the command is issued to the new process, so that the shell can be oper‐\n",
|
||
"ated interactively. If the container includes other, more advanced shells such as bash\n",
|
||
"or even a Python interpreter, they can be used as well.\n",
|
||
"A common strategy when troubleshooting containers is to create a special image\n",
|
||
"loaded with additional tools such as a debugger that can later be invoked from a shell\n",
|
||
"session.\n",
|
||
"262 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"Pushing Your Container Image to an External Registry\n",
|
||
"Having a container image locally is convenient when developing and testing an appli‐\n",
|
||
"cation, but when you are ready to share the image with others, you have to push it to\n",
|
||
"an external registry server.\n",
|
||
"The Docker Hub registry is Docker’s image repository, a convenient service where\n",
|
||
"you can host your images. A free Docker Hub account allows you to store an unlimi‐\n",
|
||
"ted number of public container images, but only one private image. Paid plans\n",
|
||
"increase the number of private images you can host. To create your Docker Hub\n",
|
||
"account, go to https://hub.docker.com.\n",
|
||
"Once you have a Docker Hub account, you can log in to it from the command line\n",
|
||
"with the docker login command:\n",
|
||
"$ docker login\n",
|
||
"Login with your Docker ID to push and pull images from Docker Hub.\n",
|
||
"Username: <your-dockerhub-username>\n",
|
||
"Password: <your-dockerhub-password>\n",
|
||
"Login Succeeded\n",
|
||
"To log in to a container image repository other than Docker Hub,\n",
|
||
"pass the address of your repository as an argument to docker\n",
|
||
"login.\n",
|
||
"Local container images are given a simple name. To prepare to push an image to\n",
|
||
"Docker Hub, the image name must be prefixed with the Docker Hub account name\n",
|
||
"and a slash as a separator. The flasky:latest image built earlier can be given a sec‐\n",
|
||
"ondary name properly formatted for pushing to Docker Hub with the docker tag\n",
|
||
"command:\n",
|
||
"$ docker tag flasky:latest <your-dockerhub-username>/flasky:latest\n",
|
||
"To upload the image to Docker Hub, use the docker push command:\n",
|
||
"$ docker push <your-dockerhub-username>/flasky:latest\n",
|
||
"The container image is now publicly available, and anybody can start a container\n",
|
||
"based on it with the docker run command:\n",
|
||
"$ docker run --name flasky -d -p 8000:5000 \\\n",
|
||
"<your-dockerhub-username>/flasky:latest\n",
|
||
"Docker Containers \n",
|
||
"| \n",
|
||
"263\n",
|
||
"\n",
|
||
"Using an External Database\n",
|
||
"One disadvantage of the way Flasky was deployed as a Docker container is that the\n",
|
||
"default SQLite database lives in the same container as the application. This makes it\n",
|
||
"very difficult to perform an upgrade, because once a running container is stopped,\n",
|
||
"the database is gone with it.\n",
|
||
"A better approach is to host the database server separately from the application con‐\n",
|
||
"tainer. That makes upgrading the application while preserving the database an easy\n",
|
||
"task, since all that is needed is to replace the application container with a new one.\n",
|
||
"Docker promotes a modular approach to building an application, in which each ser‐\n",
|
||
"vice is hosted in its own container. There are public container images available for\n",
|
||
"MySQL, Postgres, and many other database servers. The docker run command can\n",
|
||
"be used to deploy any of these directly to your system. The following command\n",
|
||
"deploys a MySQL 5.7 database server to your system:\n",
|
||
"$ docker run --name mysql -d -e MYSQL_RANDOM_ROOT_PASSWORD=yes \\\n",
|
||
" -e MYSQL_DATABASE=flasky -e MYSQL_USER=flasky \\\n",
|
||
" -e MYSQL_PASSWORD=<database-password> \\\n",
|
||
" mysql/mysql-server:5.7 \n",
|
||
"This command creates a container named mysql that runs in the background. The -e\n",
|
||
"option assigns a few environment variables that this container takes as configuration.\n",
|
||
"These and many other variables are documented in the Docker Hub page for the\n",
|
||
"MySQL image. The preceding command configures the database with a randomly\n",
|
||
"generated root password (use docker logs mysql right after starting the container to\n",
|
||
"see the assigned password in the logs), and with a brand-new database called flasky\n",
|
||
"that is configured to be accessed by a user named flasky as well. You need to provide a\n",
|
||
"secure password for this user as a value for the MYSQL_PASSWORD environment vari‐\n",
|
||
"able.\n",
|
||
"To be able to connect to a MySQL database, SQLAlchemy requires a supported\n",
|
||
"MySQL client package such as pymysql to be installed. This package can be added to\n",
|
||
"the docker.txt requirements file.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 17e to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"The change made to the requirements/docker.txt file requires the container image to\n",
|
||
"be rebuilt:\n",
|
||
"$ docker build -t flasky:latest .\n",
|
||
"264 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"If you are still running the previous application container, stop it and remove it with\n",
|
||
"docker rm -f. Then start a new container with the updated application:\n",
|
||
"$ docker run -d -p 8000:5000 --link mysql:dbserver \\\n",
|
||
" -e DATABASE_URL=mysql+pymysql://flasky:<database-password>@dbserver/flasky \\\n",
|
||
" -e MAIL_USERNAME=<your-gmail-username> -e MAIL_PASSWORD=<your-gmail-password> \\\n",
|
||
" flasky:latest\n",
|
||
"There are two additions to the docker run command shown here. The --link option\n",
|
||
"configures a connection between the new container and another existing one. The\n",
|
||
"argument to --link consists of two names separated by a colon: the source container\n",
|
||
"name or ID, and an alias for that container in the container being created. In this\n",
|
||
"example the source container is mysql, the database container started earlier. This\n",
|
||
"container is going to be accessible in the new Flasky container with the dbserver\n",
|
||
"hostname.\n",
|
||
"To complete the configuration, a DATABASE_URL environment variable is added, with a\n",
|
||
"connection URL that points to the flasky database in the mysql container. The\n",
|
||
"dbserver alias is used as the database host, as Docker makes sure that this name\n",
|
||
"resolves to the IP address of the linked container. The value of the MYSQL_PASSWORD\n",
|
||
"environment variable set in the mysql container must be included in the connection\n",
|
||
"URL for this container as well. The value of DATABASE_URL overrides the default\n",
|
||
"SQLite database, so with this simple change the container will be configured to con‐\n",
|
||
"nect to the MySQL database.\n",
|
||
"The Docker Hub repository is a gold mine of very useful applica‐\n",
|
||
"tions and services that are packaged and ready to use in a Docker\n",
|
||
"environment, either standalone or as base images for your own\n",
|
||
"containers. You will find that all sorts of projects (including data‐\n",
|
||
"bases, web servers, load balancers, programming languages, operat‐\n",
|
||
"ing systems, and more) offer official images.\n",
|
||
"Container Orchestration with Docker Compose\n",
|
||
"Containerized applications are usually composed of several running containers. You\n",
|
||
"have seen in the previous section that the main application and the database server\n",
|
||
"run in independent containers. As the application grows in complexity, it will invaria‐\n",
|
||
"bly need more containers. Some applications are going to require additional services,\n",
|
||
"such as message queues or caches. Other applications may take advantage of a micro‐\n",
|
||
"services architecture and have a distributed structure with several smaller sub-\n",
|
||
"applications, each running in its own container. Applications that have to handle high\n",
|
||
"loads or need to be fault-tolerant will want to scale out by running several instances\n",
|
||
"behind a load balancer.\n",
|
||
"Docker Containers \n",
|
||
"| \n",
|
||
"265\n",
|
||
"\n",
|
||
"As the number of containers that are part of the application increases, the task of\n",
|
||
"managing and coordinating all these containers is going to become much harder if\n",
|
||
"Docker alone is used. Container orchestration frameworks built on top of Docker help\n",
|
||
"with this task.\n",
|
||
"The Compose toolset provides basic orchestration functionality, included with the\n",
|
||
"Docker installation. With Compose, the containers that are part of an application are\n",
|
||
"described in a configuration file, typically named docker-compose.yml. The docker-\n",
|
||
"compose command can then start all the containers associated with the application\n",
|
||
"using a single command.\n",
|
||
"Example 17-12 shows a docker-compose.yml file that represents the containerized Fla‐\n",
|
||
"sky along with its MySQL service.\n",
|
||
"Example 17-12. docker-compose.yml: compose configuration\n",
|
||
"version: '3'\n",
|
||
"services:\n",
|
||
" flasky:\n",
|
||
" build: .\n",
|
||
" ports:\n",
|
||
" - \"8000:5000\"\n",
|
||
" env_file: .env\n",
|
||
" links:\n",
|
||
" - mysql:dbserver\n",
|
||
" restart: always\n",
|
||
" mysql:\n",
|
||
" image: \"mysql/mysql-server:5.7\"\n",
|
||
" env_file: .env-mysql\n",
|
||
" restart: always\n",
|
||
"This file is written in YAML, which is a clean and simple format that can represent\n",
|
||
"hierarchical structures that are composed of key-value maps and lists. The version\n",
|
||
"key specifies which version of Compose is used, and the services key defines the\n",
|
||
"containers of the application as its children. In the case of Flasky, these are two serv‐\n",
|
||
"ices named flasky and mysql.\n",
|
||
"For services such as flasky, which are built as part of the application, the subkeys\n",
|
||
"specify the arguments that are given to the docker build and docker run com‐\n",
|
||
"mands. The build key specifies the build directory, where the Dockerfile is located.\n",
|
||
"The ports key specifies the network port mappings. The env_file key is a conve‐\n",
|
||
"nient way to define several environment variables that the container needs. The links\n",
|
||
"key establishes a link to the MySQL container, by exposing it with the hostname\n",
|
||
"dbserver. The restart key set to always provides a simple way for Docker to auto‐\n",
|
||
"matically restart the container if it exits unexpectedly. The .env file for this deploy‐\n",
|
||
"ment should have the following variables in it:\n",
|
||
"266 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"FLASK_APP=flasky.py\n",
|
||
"FLASK_CONFIG=docker\n",
|
||
"SECRET_KEY=3128b4588e7f4305b5501025c13ceca5\n",
|
||
"MAIL_USERNAME=<your-gmail-username>\n",
|
||
"MAIL_PASSWORD=<your-gmail-password>\n",
|
||
"DATABASE_URL=mysql+pymysql://flasky:<database-password>@dbserver/flasky\n",
|
||
"The mysql service has a simpler structure, because this is a service that is started from\n",
|
||
"a stock image that does not require a build step. The image key specifies the name\n",
|
||
"and tag of the container image to use for this service. As with the docker run com‐\n",
|
||
"mand, Docker will download this image from the container image registry. The\n",
|
||
"env_file and restart keys are similar to those used in the flasky container. Note\n",
|
||
"how the environment variables for the MySQL container are stored in a separate file\n",
|
||
"named .env-mysql. While it would be easier to add the environment variables needed\n",
|
||
"by all containers to the .env file, it is a good practice to prevent one container from\n",
|
||
"having access to the secrets of another. The .env-mysql file needs the following envi‐\n",
|
||
"ronment variables defined:\n",
|
||
"MYSQL_RANDOM_ROOT_PASSWORD=yes\n",
|
||
"MYSQL_DATABASE=flasky\n",
|
||
"MYSQL_USER=flasky\n",
|
||
"MYSQL_PASSWORD=<database-password>\n",
|
||
"The .env and .env-mysql files contain passwords and other sensitive\n",
|
||
"information, so they should never be added to source control.\n",
|
||
"A complete reference for the docker-compose.yml file is found at\n",
|
||
"“the Docker website”.\n",
|
||
"A typical problem with orchestrated systems is that containers are started in the\n",
|
||
"wrong order—or in the correct order, but without giving the containers for base serv‐\n",
|
||
"ices enough time to start and initialize before starting higher-level containers that\n",
|
||
"depend on them. In the case of Flasky, the mysql container needs to start first, so that\n",
|
||
"the database is up and running when the flasky container starts. Then it can connect\n",
|
||
"to the database, apply the database migrations, and finally start the web server.\n",
|
||
"Compose will start the mysql and flasky containers in the right order, because it will\n",
|
||
"detect the dependency between them from the links key in the flasky container. But\n",
|
||
"Compose is not going to wait for MySQL to start, which might take a few seconds.\n",
|
||
"When designing distributed systems, it is a good practice to implement retries in all\n",
|
||
"connections to external services. Example 17-13 shows how the boot.sh script that\n",
|
||
"Docker Containers \n",
|
||
"| \n",
|
||
"267\n",
|
||
"\n",
|
||
"starts the flasky container can be made more robust by retrying the flask deploy\n",
|
||
"command, which retries the database upgrade until it succeeds.\n",
|
||
"Example 17-13. boot.sh: waiting for the database to be up\n",
|
||
"#!/bin/sh\n",
|
||
"source venv/bin/activate\n",
|
||
"while true; do\n",
|
||
" flask deploy\n",
|
||
" if [[ \"$?\" == \"0\" ]]; then\n",
|
||
" break\n",
|
||
" fi\n",
|
||
" echo Deploy command failed, retrying in 5 secs...\n",
|
||
" sleep 5\n",
|
||
"done\n",
|
||
"exec gunicorn -b :5000 --access-logfile - --error-logfile - flasky:app\n",
|
||
"By running flask deploy inside a retry loop, the container will be able to tolerate\n",
|
||
"failures due to the database service not being immediately ready to accept requests.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 17f to check out this version of the applica‐\n",
|
||
"tion. Also make sure that the .env and .env-mysql environment files\n",
|
||
"are created and populated with values correct for your environ‐\n",
|
||
"ment.\n",
|
||
"Now that the Compose configuration is complete, the application can be started with\n",
|
||
"the docker-compose up command:\n",
|
||
"$ docker-compose up -d --build\n",
|
||
"The --build option to docker-compose up indicates that a build step should run\n",
|
||
"before launching the application. This will cause the flasky container image to be\n",
|
||
"built. After the image is created, the mysql and flasky containers will be started in\n",
|
||
"that order. The -d option starts the containers in detached mode, as with the single\n",
|
||
"container. After a few seconds, the application should be up and running in the back‐\n",
|
||
"ground, and you should be able to connect to it at http://localhost:8000.\n",
|
||
"Compose consolidates the logging from all the containers into a single stream, which\n",
|
||
"you can see with the docker-compose logs command:\n",
|
||
"$ docker-compose logs\n",
|
||
"Or, if you want to constantly monitor the log stream:\n",
|
||
"$ docker-compose logs -f\n",
|
||
"268 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"The docker-compose ps command shows a summary of all the application contain‐\n",
|
||
"ers that are running and their state:\n",
|
||
"$ docker-compose ps\n",
|
||
" Name Command State Ports\n",
|
||
"------------------------------------------------------------------------\n",
|
||
"flasky_flasky_1 ./boot.sh Up 0.0.0.0:8000->5000/tcp\n",
|
||
"flasky_mysql_1 /entrypoint.sh mysqld Up 3306/tcp, 33060/tcp\n",
|
||
"To upgrade an application to a new version, simply make the necessary changes to it\n",
|
||
"and repeat the docker-compose up command used previously to start it. Compose\n",
|
||
"will rebuild the application container if anything changed, and then replace the older\n",
|
||
"container with a fresh one.\n",
|
||
"To stop the application, use the docker-compose down command, or docker-compose\n",
|
||
"rm --stop --force if you also want to remove the stopped containers.\n",
|
||
"Cleaning Up Old Containers and Images\n",
|
||
"As you work with containers, your system will invariably accumulate old containers\n",
|
||
"or images that are not needed anymore. It is a good idea to routinely review and clean\n",
|
||
"those up, so that they don’t take up space on the system.\n",
|
||
"To see the list of containers in the system, use the following command:\n",
|
||
"$ docker ps -a\n",
|
||
"This will show containers that are running, and containers that were stopped but are\n",
|
||
"still in the system. To delete any containers from this list, use the docker rm -f com‐\n",
|
||
"mand and provide the names or IDs to remove:\n",
|
||
"$ docker rm -f <name-or-id> <name-or-id> ...\n",
|
||
"To see the list of container images stored in your system, use the docker images\n",
|
||
"command. If there are any images that you want to remove, you can do so with the\n",
|
||
"docker rmi command.\n",
|
||
"Some containers create virtual volumes on the host computer that are used for storage\n",
|
||
"outside of the container filesystem. The MySQL container image, for example, puts all\n",
|
||
"the database files in a volume. You can view a list of all the allocated volumes in your\n",
|
||
"system with docker volume ls. To remove a volume that is unused, use docker\n",
|
||
"volume rm.\n",
|
||
"If you prefer a more automatic cleanup, the docker system prune --volumes com‐\n",
|
||
"mand will remove any unused images or volumes, and any stopped containers that\n",
|
||
"are still in the system.\n",
|
||
"Docker Containers \n",
|
||
"| \n",
|
||
"269\n",
|
||
"\n",
|
||
"Using Docker in Production\n",
|
||
"Many people consider Docker a development and testing platform only. While the\n",
|
||
"techniques presented in the previous sections can be used to deploy applications on\n",
|
||
"production servers running Docker, there are some limitations and security concerns\n",
|
||
"that need to be considered:\n",
|
||
"Monitoring and alerting\n",
|
||
"What happens if a containerized application crashes? Docker can restart a con‐\n",
|
||
"tainer that exits unexpectedly, but it will not monitor your containers, nor will it\n",
|
||
"send alerts when they behave erratically.\n",
|
||
"Logging\n",
|
||
"Docker maintains a separate log stream for each container. Compose improves\n",
|
||
"this by offering a consolidated stream, but without long-term storage or search‐\n",
|
||
"ing and filtering capabilities.\n",
|
||
"Management of secrets\n",
|
||
"Configuring passwords and other credentials through environment variables is\n",
|
||
"insecure, since Docker exposes pre-configured environment variables via the\n",
|
||
"docker inspect command or through its API.\n",
|
||
"Reliability and scaling\n",
|
||
"To help with fault tolerance, or to accommodate increasing load demands, it is\n",
|
||
"necessary to run several instances of the application on several hosts and behind\n",
|
||
"one or more load balancers.\n",
|
||
"These limitations are generally addressed by more elaborated orchestration frame‐\n",
|
||
"works built on top of Docker or other container runtimes. Frameworks such as\n",
|
||
"Docker Swarm (now incorporated into Docker), Apache Mesos, and Kubernetes are\n",
|
||
"good choices for building robust container deployments.\n",
|
||
"Traditional Deployments\n",
|
||
"So far you have seen how Heroku and Docker manage deployments. To complete this\n",
|
||
"review of deployment strategies, this section will describe a traditional hosting\n",
|
||
"option, which involves buying or renting a server, either physical or virtual, and man‐\n",
|
||
"ually setting up all the required components on it. This is obviously the most labori‐\n",
|
||
"ous option of all, but it can be a convenient option when you have terminal access to\n",
|
||
"production server hardware. The following sections will give you an idea of the work\n",
|
||
"involved.\n",
|
||
"270 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"Server Setup\n",
|
||
"There are several administration tasks that must be performed on the server before it\n",
|
||
"can host applications:\n",
|
||
"• Install a database server such as MySQL or Postgres. Using an SQLite database is\n",
|
||
"also possible but is not recommended for a production server due to its many\n",
|
||
"limitations with regard to modification of existing database schemas.\n",
|
||
"• Install a Mail Transport Agent (MTA) such as Sendmail or Postfix to send email\n",
|
||
"out to users. Using Gmail in a production application is not possible, as this ser‐\n",
|
||
"vice has very restrictive quotas and specifically prohibits commercial use in its\n",
|
||
"terms of service.\n",
|
||
"• Install a production-ready web server such as Gunicorn or uWSGI.\n",
|
||
"• Install a process-monitoring utility such as Supervisor, that immediately restarts\n",
|
||
"the web server if it crashes or after the host is power-cycled.\n",
|
||
"• Install and configure an SSL certificate to enable secure HTTP.\n",
|
||
"• (Optional but highly recommended) Install a front-end reverse proxy web server\n",
|
||
"such as nginx or Apache. This server is configured to serve static files directly and\n",
|
||
"forward application requests into the application’s web server, which is listening\n",
|
||
"on a private port on localhost.\n",
|
||
"• Secure the server. This includes several tasks that have the goal of reducing vul‐\n",
|
||
"nerabilities in the server such as installing firewalls, removing unused software\n",
|
||
"and services, and so on.\n",
|
||
"Instead of manually performing these tasks, create a scripted\n",
|
||
"deployment using an automation framework such as Ansible, Chef,\n",
|
||
"or Puppet.\n",
|
||
"Importing Environment Variables\n",
|
||
"Similarly to Heroku and Docker, an application running on a standalone server relies\n",
|
||
"on certain settings such as the database connection URL, email server credentials, etc.\n",
|
||
"being provided in environment variables.\n",
|
||
"Because there is no Heroku or Docker to configure these variables before the applica‐\n",
|
||
"tion starts, the procedure to set the variables is dependent on the platform and tools\n",
|
||
"used. To make the configuration of environment variables easier and uniform across\n",
|
||
"deployment platforms, the short code block in Example 17-14 imports into the envi‐\n",
|
||
"ronment a .env file similar to the one used with the heroku local and docker-\n",
|
||
"Traditional Deployments \n",
|
||
"| \n",
|
||
"271\n",
|
||
"\n",
|
||
"compose commands, using a Python package called python-dotenv that needs to be\n",
|
||
"installed with pip. This is done in flasky.py before the application instance is created,\n",
|
||
"so that by the time the configuration is imported these variables are accessible in the\n",
|
||
"environment.\n",
|
||
"Example 17-14. flasky.py: importing the environment from the .env file\n",
|
||
"import os\n",
|
||
"from dotenv import load_dotenv\n",
|
||
"dotenv_path = os.path.join(os.path.dirname(__file__), '.env')\n",
|
||
"if os.path.exists(dotenv_path):\n",
|
||
" load_dotenv(dotenv_path)\n",
|
||
"The .env file can define the FLASK_CONFIG variable that selects the configuration to\n",
|
||
"use, the DATABASE_URL connection, the email server credentials, etc. As explained\n",
|
||
"before, a .env file should not be added to source control due to the sensitive nature of\n",
|
||
"some of the items in it.\n",
|
||
"If you created a .env file for use with Heroku or Docker, review it\n",
|
||
"and adjust it appropriately, because with the changes just made, the\n",
|
||
"application will import the variables defined in this file for all con‐\n",
|
||
"figurations.\n",
|
||
"Setting Up Logging\n",
|
||
"For Unix-based servers, logging can be sent to the syslog daemon. A new configura‐\n",
|
||
"tion specifically for Unix can be created as a subclass of ProductionConfig, as shown\n",
|
||
"in Example 17-15.\n",
|
||
"Example 17-15. config.py: Unix example configuration\n",
|
||
"class UnixConfig(ProductionConfig):\n",
|
||
" @classmethod\n",
|
||
" def init_app(cls, app):\n",
|
||
" ProductionConfig.init_app(app)\n",
|
||
" # log to syslog\n",
|
||
" import logging\n",
|
||
" from logging.handlers import SysLogHandler\n",
|
||
" syslog_handler = SysLogHandler()\n",
|
||
" syslog_handler.setLevel(logging.WARNING)\n",
|
||
" app.logger.addHandler(syslog_handler)\n",
|
||
"With this configuration, application logs will be written to the configured syslog mes‐\n",
|
||
"sages file, typically /var/log/messages or /var/log/syslog depending on the Linux distri‐\n",
|
||
"272 \n",
|
||
"| \n",
|
||
"Chapter 17: Deployment\n",
|
||
"\n",
|
||
"bution. The syslog service can be configured to write a separate log file for application\n",
|
||
"logs, or to send the logs to a different machine if desired.\n",
|
||
"If you have cloned the application’s Git repository on GitHub, you\n",
|
||
"can run git checkout 17g to check out this version of the applica‐\n",
|
||
"tion.\n",
|
||
"Traditional Deployments \n",
|
||
"| \n",
|
||
"273\n",
|
||
"\n",
|
||
"\n",
|
||
"CHAPTER 18\n",
|
||
"Additional Resources\n",
|
||
"You are pretty much done with this book. Congratulations! I hope the topics that I\n",
|
||
"have covered have given you a solid base to begin building your own applications\n",
|
||
"with Flask. The code examples are open source and have a permissive license, so you\n",
|
||
"are welcome to use as much of my code as you want to seed your projects, even if\n",
|
||
"they are of a commercial nature. In this short final chapter, I want to give you a list of\n",
|
||
"additional tips and resources that might be useful as you continue working with\n",
|
||
"Flask.\n",
|
||
"Using an Integrated Development Environment (IDE)\n",
|
||
"Developing Flask applications in an integrated development environment (IDE) can\n",
|
||
"be very convenient, since features such as code completion and an interactive debug‐\n",
|
||
"ger can speed up the coding process considerably. Some of the IDEs that work well\n",
|
||
"with Flask are listed here:\n",
|
||
"PyCharm\n",
|
||
"IDE from JetBrains with Community (free) and Professional (paid) editions,\n",
|
||
"both compatible with Flask applications. Available on Linux, macOS, and Win‐\n",
|
||
"dows.\n",
|
||
"Visual Studio Code\n",
|
||
"Open source IDE from Microsoft. A third-party Python plug-in must be installed\n",
|
||
"to have access to code completion and debugging features with Flask applica‐\n",
|
||
"tions. Available on Linux, macOS, and Windows.\n",
|
||
"PyDev\n",
|
||
"Open source IDE based on Eclipse. Available on Linux, macOS, and Windows.\n",
|
||
"275\n",
|
||
"\n",
|
||
"Finding Flask Extensions\n",
|
||
"The examples in this book rely on several extensions and packages, but there are\n",
|
||
"many more that are also useful and were not discussed. The following is a short list of\n",
|
||
"some additional packages that are worth exploring:\n",
|
||
"• Flask-Babel: Internationalization and localization support\n",
|
||
"• Marshmallow: Serialization and deserialization of Python objects, useful for API\n",
|
||
"resource representations\n",
|
||
"• Celery: Task queue for processing background jobs\n",
|
||
"• Frozen-Flask: Conversion of a Flask application to a static website\n",
|
||
"• Flask-DebugToolbar: In-browser debugging tools\n",
|
||
"• Flask-Assets: Merging, minifying, and compiling of CSS and JavaScript assets\n",
|
||
"• Flask-Session: Alternative implementation of user sessions that use server-side\n",
|
||
"storage\n",
|
||
"• Flask-SocketIO: Socket.IO server implementation with support for WebSocket\n",
|
||
"and long-polling\n",
|
||
"If the functionality that you need for your project is not covered by any of the exten‐\n",
|
||
"sions and packages mentioned in this book, then your first destination to look for\n",
|
||
"additional extensions should be the official Flask Extension Registry. Other good\n",
|
||
"places to search are the Python Package Index, GitHub, and Bitbucket.\n",
|
||
"Getting Help\n",
|
||
"If you reach a point where you are blocked by an issue that you cannot resolve on\n",
|
||
"your own, you should keep in mind that there is a community of Flask developers\n",
|
||
"like you that will be happy to help you.\n",
|
||
"A great place to ask questions about Flask or any related extensions is Stack Overflow.\n",
|
||
"Other developers that see your question and know how to answer will post their\n",
|
||
"answers, which are voted up or down according to their quality. You, as the owner of\n",
|
||
"the question, can then select the best answer. All questions and their answers stay on\n",
|
||
"the site and appear in search results. So, by asking your question on this platform,\n",
|
||
"you help grow the collection of information about Flask.\n",
|
||
"Reddit also has a friendly Flask-dedicated subreddit where you can post questions.\n",
|
||
"Finally, if you use IRC, the #pocoo channel on Freenode is frequented by Flask devel‐\n",
|
||
"opers of all levels who can help you one-on-one with your problem.\n",
|
||
"276 \n",
|
||
"| \n",
|
||
"Chapter 18: Additional Resources\n",
|
||
"\n",
|
||
"Getting Involved with Flask\n",
|
||
"Flask would not be as awesome as it is without the work done by its community of\n",
|
||
"developers. As you are now becoming part of this community and benefiting from\n",
|
||
"the work of so many volunteers, you should consider finding a way to give something\n",
|
||
"back. Here are some ideas to help you get started:\n",
|
||
"• Review the documentation for Flask or your favorite related project and submit\n",
|
||
"corrections or improvements.\n",
|
||
"• Translate the documentation into a new language.\n",
|
||
"• Answer questions on Q&A sites such as Stack Overflow.\n",
|
||
"• Talk about your work with your peers at user group meetings or conferences.\n",
|
||
"• Contribute bug fixes or improvements to packages that you use.\n",
|
||
"• Write new Flask extensions and release them as open source.\n",
|
||
"• Release your applications as open source.\n",
|
||
"I hope you decide to volunteer in one of these ways, or any others that are meaningful\n",
|
||
"to you. If you do, thank you!\n",
|
||
"Getting Involved with Flask \n",
|
||
"| \n",
|
||
"277\n",
|
||
"\n",
|
||
"\n",
|
||
"Index\n",
|
||
"A\n",
|
||
"abort function, 22, 231\n",
|
||
"absolute URLs (in links), 37\n",
|
||
"account confirmation, 118-125\n",
|
||
"generating confirmation tokens with its‐\n",
|
||
"dangerous, 118\n",
|
||
"sending confirmation emails, 120-124\n",
|
||
"account management, 125\n",
|
||
"role assignment to users, 135\n",
|
||
"activation, virtual environments, 4\n",
|
||
"administrator roles, 127\n",
|
||
"assignment of, 131\n",
|
||
"administrators, user profile editor for, 143-145\n",
|
||
"admin_required decorator, 145\n",
|
||
"after_app_request hook, 238\n",
|
||
"after_request hook, 20\n",
|
||
"Alembic database migrations, 74\n",
|
||
"Alpine Linux, 258\n",
|
||
"AnonymousUser custom class, 132\n",
|
||
"Apache Mesos, 270\n",
|
||
"APIs (application programming interfaces),\n",
|
||
"199-218\n",
|
||
"introduction to REST, 199-203\n",
|
||
"resources, 200\n",
|
||
"versioning web services, 202\n",
|
||
"RESTful web services with Flask, 203-218\n",
|
||
"app.add_url_rule method, 8, 19\n",
|
||
"app.cli.command decorator, 96\n",
|
||
"app.config object, 44\n",
|
||
"app.run method, 11\n",
|
||
"app.shell_context_processor decorator, 72\n",
|
||
"application context, 18\n",
|
||
"and email on background thread, 83\n",
|
||
"application directory, creating, 2\n",
|
||
"application instance, 7\n",
|
||
"application structure, basic, 7-23\n",
|
||
"command-line options, 15\n",
|
||
"complete application example, 9\n",
|
||
"debug mode, 13\n",
|
||
"development server, 10\n",
|
||
"dynamic routes, 12\n",
|
||
"Flask extensions, 23\n",
|
||
"initializing applications, 7\n",
|
||
"request-response cycle, 17-22\n",
|
||
"routes and view functions, 8\n",
|
||
"applications, large, structure of, 85-97\n",
|
||
"application package, 88-92\n",
|
||
"implementing application functionality\n",
|
||
"in a blueprint, 90-92\n",
|
||
"using an application factory, 88-89\n",
|
||
"application script, 93\n",
|
||
"configuration options, 86-88\n",
|
||
"database setup, 96\n",
|
||
"project structure, 85\n",
|
||
"requirements file, 93\n",
|
||
"running the application, 97\n",
|
||
"unit tests, 94\n",
|
||
"app_errorhandler decorator, 91, 205\n",
|
||
"association tables (database), 65, 172\n",
|
||
"followers table as a model, 174\n",
|
||
"auth.login_required decorator, 208\n",
|
||
"authentication, 101-125\n",
|
||
"account confirmation, 118-125\n",
|
||
"account management, 125\n",
|
||
"creating blueprint for, 105\n",
|
||
"Flask extensions for, 101\n",
|
||
"Flask-Mail Gmail account, 80\n",
|
||
"in API blueprint, 204\n",
|
||
"279\n",
|
||
"\n",
|
||
"new user registration, 115-118\n",
|
||
"password security, 102-105\n",
|
||
"token-based, 208-210\n",
|
||
"user authentication with Flask-Login,\n",
|
||
"107-115\n",
|
||
"with Flask-HTTPAuth, 206\n",
|
||
"with Heroku account, 245\n",
|
||
"automation frameworks, 271\n",
|
||
"avatars, 146-149\n",
|
||
"avatar in profile page, 147\n",
|
||
"blog post author, 153\n",
|
||
"Gravatar query string arguments, 146\n",
|
||
"Gravatar URL generation, 146\n",
|
||
"AWS EC2, 243\n",
|
||
"B\n",
|
||
"background jobs, for sending email, 84\n",
|
||
"background thread, email on, 83\n",
|
||
"bash, 2\n",
|
||
"before_app_request decorator, 122, 138\n",
|
||
"before_first_request hook, 20\n",
|
||
"before_request hook, 20, 122\n",
|
||
"before_request handler with authentication,\n",
|
||
"208\n",
|
||
"Bleach package, 164\n",
|
||
"blocks (in base templates), 29\n",
|
||
"available blocks in Flash-Bootstrap, 32\n",
|
||
"blog posts, 151-169\n",
|
||
"editor for, 167-169\n",
|
||
"on profile pages, 154\n",
|
||
"paginating long lists of, 155-161\n",
|
||
"permanent links to, 165-167\n",
|
||
"querying followed posts using database join,\n",
|
||
"181-183\n",
|
||
"rich text posts with Markdown and Flask-\n",
|
||
"PageDown, 161-165\n",
|
||
"showing followed posts on home page,\n",
|
||
"183-187\n",
|
||
"submission and display of, 151-154\n",
|
||
"blueprints, 90-92\n",
|
||
"creating, 90\n",
|
||
"creating authentication blueprint, 105\n",
|
||
"creating for RESTful API, 203\n",
|
||
"error handlers in, 91\n",
|
||
"registration with app in factory function, 91\n",
|
||
"RESTful API blueprint registration, 204\n",
|
||
"routes in, 91\n",
|
||
"BooleanField class, 109\n",
|
||
"boot.sh (container startup script), 259\n",
|
||
"Bootstrap, integration using Flash-Bootstrap,\n",
|
||
"30-33\n",
|
||
"business logic, 25\n",
|
||
"C\n",
|
||
"cardinality, 58\n",
|
||
"Click package, 1\n",
|
||
"cloud, deployment to, 243\n",
|
||
"code coverage reports, 221-224\n",
|
||
"code examples from this book, xiii\n",
|
||
"collections (NoSQL databases, 58\n",
|
||
"columns (database), 57\n",
|
||
"db.Column class, 62\n",
|
||
"SQLAlchemy column options, 63\n",
|
||
"SQLAlchemy column types, 63\n",
|
||
"command-line interface (CLI), installing for\n",
|
||
"Heroku, 245\n",
|
||
"command-line options, flask command, 15\n",
|
||
"comments (user) (see user comments)\n",
|
||
"committing database sessions, 67\n",
|
||
"conditional statements (in templates), 28, 48\n",
|
||
"Config class, 87\n",
|
||
"configuration\n",
|
||
"applications created by application script, 93\n",
|
||
"applications created by factory function, 89\n",
|
||
"applications deployed on Heroku platform,\n",
|
||
"247\n",
|
||
"configuring email for applications on Her‐\n",
|
||
"oku, 248\n",
|
||
"Docker deployment, 258\n",
|
||
"for slow query reporting, 238\n",
|
||
"large applications, options for, 86-88\n",
|
||
"sending email for application errors, 242\n",
|
||
"Unix-based servers, logging, 272\n",
|
||
"containers, 244, 256\n",
|
||
"(see also Docker)\n",
|
||
"content negotiation, 205\n",
|
||
"context processors, 134\n",
|
||
"contexts, 17\n",
|
||
"for email on background thread, 83\n",
|
||
"shell context, adding, 72\n",
|
||
"control structures (Jinja2), 28\n",
|
||
"cookies\n",
|
||
"client-side, user sessions in, 52\n",
|
||
"setting in response object, 21\n",
|
||
"showing followed posts, 184\n",
|
||
"Coordinated Universal Time (UTC), 38\n",
|
||
"coverage tool, 222\n",
|
||
"(see also code coverage reports)\n",
|
||
"280 \n",
|
||
"| \n",
|
||
"Index\n",
|
||
"\n",
|
||
"create_app factory function\n",
|
||
"attaching authentication blueprint to app,\n",
|
||
"106\n",
|
||
"cross-site request forgery (CSRF) attacks, 44\n",
|
||
"CSS\n",
|
||
"Bootstrap CSS files, 30, 33\n",
|
||
"Bootstrap pagination classes, 159\n",
|
||
"classes for avatar in profile page, 147\n",
|
||
"styles for blog posts, 153\n",
|
||
"cURL, 217\n",
|
||
"current_time variable, 39\n",
|
||
"current_user context variable, 113, 142\n",
|
||
"current_user.can function, 132\n",
|
||
"current_user.is_administrator function, 132\n",
|
||
"current_user.is_authenticated property, 110,\n",
|
||
"114\n",
|
||
"current_user._get_current_object method, 152\n",
|
||
"Cygwin, 2\n",
|
||
"D\n",
|
||
"database (in database URLs), 61\n",
|
||
"databases, 57-77\n",
|
||
"creating tables, 66\n",
|
||
"deleting rows, 68\n",
|
||
"Flask support for, xi\n",
|
||
"inserting rows, 66\n",
|
||
"integration with Python shell, 72\n",
|
||
"large applications using different databases,\n",
|
||
"87\n",
|
||
"logging slow performance, 237-239\n",
|
||
"making users their own followers in the\n",
|
||
"database, 186\n",
|
||
"management with Flask-SQLAlchemy, 61\n",
|
||
"migrations with Flask-Migrate, 73-77\n",
|
||
"adding more migrations, 76\n",
|
||
"upgrading the database, 75\n",
|
||
"model definition, 62-64\n",
|
||
"modifying rows, 68\n",
|
||
"NoSQL, 58\n",
|
||
"of user roles, 127-131\n",
|
||
"adding new roles in shell session, 134\n",
|
||
"Post model for blog posts, 151\n",
|
||
"Markdown text handling, 164\n",
|
||
"provisioning a database on Heroku, 246\n",
|
||
"Python database frameworks, 59-60\n",
|
||
"querying followed posts using database join,\n",
|
||
"181-183\n",
|
||
"filter_by query filter, 182\n",
|
||
"join query filter, 182\n",
|
||
"querying rows, 68\n",
|
||
"relational model, 57\n",
|
||
"relationships in, 64-65, 171-178\n",
|
||
"association table, 172\n",
|
||
"representation of blog post comments,\n",
|
||
"189-191\n",
|
||
"setup in a large application, 96\n",
|
||
"SQL, 57\n",
|
||
"SQL vs. NoSQL, 59\n",
|
||
"storing MD5 hashes for user avatars, 148\n",
|
||
"use in view functions, 71-72\n",
|
||
"user information in, 137\n",
|
||
"user, password hashes stored in, 102\n",
|
||
"using an external database with Docker con‐\n",
|
||
"tainers, 264\n",
|
||
"DATABASE_URL environment variable, 246,\n",
|
||
"265\n",
|
||
"DataRequired form field validator, 45, 49, 109\n",
|
||
"dates and time\n",
|
||
"last visit date for users, 138\n",
|
||
"localization with Flask-Moment, 38\n",
|
||
"timestamps in user information database,\n",
|
||
"137\n",
|
||
"db object, 62\n",
|
||
"db.Column class, 62\n",
|
||
"db.create_all function, 66, 75\n",
|
||
"db.ForeignKey function, 64\n",
|
||
"db.relationship function, 64\n",
|
||
"db.session object, 67\n",
|
||
"db.session.add method, 68\n",
|
||
"db.session.commit method, 67, 121\n",
|
||
"db.session.delete method, 68\n",
|
||
"db.session.rollback method, 68\n",
|
||
"debugging\n",
|
||
"debug mode, 13, 93\n",
|
||
"--debugger and --no-debugger command-\n",
|
||
"line options, 16\n",
|
||
"of errors during production, 242\n",
|
||
"debugging subsystem, 1\n",
|
||
"decorators, 8\n",
|
||
"custom, checking user permissions, 132\n",
|
||
"order of, in view functions using multiple\n",
|
||
"decorators, 133\n",
|
||
"request hooks implemented as, 20\n",
|
||
"DELETE request method (HTTP), 201\n",
|
||
"denormalization (NoSQL databases), 59\n",
|
||
"dependencies, 1\n",
|
||
"for applications deployed on Heroku, 248\n",
|
||
"for development vs. production, 156\n",
|
||
"Index \n",
|
||
"| \n",
|
||
"281\n",
|
||
"\n",
|
||
"installing into virtual environment with pip,\n",
|
||
"5\n",
|
||
"requirements file for large applications, 93\n",
|
||
"deploy command, 241\n",
|
||
"deployment, 241-273\n",
|
||
"Docker containers, 256-270\n",
|
||
"in the cloud, 243\n",
|
||
"logging errors during production, 242\n",
|
||
"on Heroku platform, 244-255\n",
|
||
"preparing the application, 244-253\n",
|
||
"traditional, 270-273\n",
|
||
"importing environment variables, 271\n",
|
||
"server setup, 271\n",
|
||
"setting up logging, 272\n",
|
||
"workflow, 241\n",
|
||
"deserialization (APIs), 212\n",
|
||
"development web server, 10\n",
|
||
"DEV_DATABASE_URL environment variable,\n",
|
||
"96\n",
|
||
"Docker, 244, 256-270\n",
|
||
"building a Docker image, 257\n",
|
||
"container orchestration with Compose,\n",
|
||
"265-269\n",
|
||
"Dockerfile, 257\n",
|
||
"images command, 260, 269\n",
|
||
"installing, 256\n",
|
||
"login command, 263\n",
|
||
"logs command, 262, 264\n",
|
||
"ps command, 269\n",
|
||
"push command, 263\n",
|
||
"rm command, 262, 269\n",
|
||
"rmi command, 269\n",
|
||
"run command, 261, 264\n",
|
||
"stop command, 262\n",
|
||
"Swarm, 270\n",
|
||
"system command, 269\n",
|
||
"tag command, 263\n",
|
||
"using an external database, 264\n",
|
||
"using in production, 270\n",
|
||
"version command, 256\n",
|
||
"volume command, 269\n",
|
||
"Docker Compose, 265\n",
|
||
"docker-compose.yml file, 266\n",
|
||
"logs command, 268\n",
|
||
"ps command, 269\n",
|
||
"up command, 268\n",
|
||
"Docker Hub, 263\n",
|
||
"applications and services on, 265\n",
|
||
"document-oriented databases, 57\n",
|
||
"dynamic routes, 9\n",
|
||
"dynamic URLs, generating with url_for func‐\n",
|
||
"tion, 37\n",
|
||
"dynos (Heroku), 244\n",
|
||
"E\n",
|
||
"EC2 service (AWS), 243\n",
|
||
"email, 79\n",
|
||
"(see also Flask-Mail)\n",
|
||
"configuring for applications on Heroku, 248\n",
|
||
"configuring for Docker container, 261\n",
|
||
"confirming user accounts, 120-124\n",
|
||
"handling address changes for user accounts,\n",
|
||
"125\n",
|
||
"sending for application errors, 242\n",
|
||
"entity-relationship diagrams, 58\n",
|
||
".env file, 253, 266-267, 271\n",
|
||
"environment variables\n",
|
||
"defined in Dockerfile, 258\n",
|
||
"importing in traditional deployment, 271\n",
|
||
"in large application configuration, 87\n",
|
||
"EqualTo form field validator, 116\n",
|
||
"error handling\n",
|
||
"API error handler for ValidationError, 212\n",
|
||
"error handlers in app blueprints, 91\n",
|
||
"Flask-HTTPAuth error handler, 208\n",
|
||
"in RESTful web services, 204\n",
|
||
"with content negotiation, 205\n",
|
||
"error pages, custom, 33-36\n",
|
||
"errors, logging during production, 242\n",
|
||
"extends directive, 30, 32\n",
|
||
"extensibility of Flask, xi\n",
|
||
"extensions, 1, 23, 30\n",
|
||
"additional Flask extensions and packages,\n",
|
||
"276\n",
|
||
"initialization, 31\n",
|
||
"F\n",
|
||
"factory function, creating applications with, 88\n",
|
||
"fake blog post data, creating, 155-157\n",
|
||
"Faker package, 155\n",
|
||
"filters (database), 27\n",
|
||
"offset() query filter, 157\n",
|
||
"query of followed posts using database join,\n",
|
||
"183\n",
|
||
"using with database queries, 68\n",
|
||
"filter_by method, 69\n",
|
||
"SQLAlchemy filters for, 69\n",
|
||
"flash function, 53\n",
|
||
"282 \n",
|
||
"| \n",
|
||
"Index\n",
|
||
"\n",
|
||
"Flask\n",
|
||
"application factory function, 88\n",
|
||
"app_errorhandler decorator, 91, 205\n",
|
||
"basic multiple-file application structure, 85\n",
|
||
"custom commands, 96\n",
|
||
"dynamic routes, 9\n",
|
||
"extensions, 30\n",
|
||
"Flask class, 7\n",
|
||
"flask command options, 15\n",
|
||
"installing into virtual environment with pip,\n",
|
||
"5\n",
|
||
"server shutdown, 230\n",
|
||
"test client, 224-229\n",
|
||
"working with, additional resources for,\n",
|
||
"275-277\n",
|
||
"flask db downgrade command, 76\n",
|
||
"flask db migrate command, 75-76\n",
|
||
"flask db stamp command, 76\n",
|
||
"flask db upgrade command, 75-76\n",
|
||
"flask deploy command, 268\n",
|
||
"Flask Extension Registry, 276\n",
|
||
"flask run command, 10\n",
|
||
"--host argument, 16\n",
|
||
"options, 16\n",
|
||
"flask shell command, 15, 66, 72\n",
|
||
"flask test command, --coverage option, 222\n",
|
||
"Flask-Bootstrap, 30-33\n",
|
||
"blocks, 29\n",
|
||
"initialization, 30\n",
|
||
"installing, 30\n",
|
||
"quick_form macro, 47\n",
|
||
"wtf.quick_form Jinja2 macro, 110\n",
|
||
"wtf.quick_form method, 47\n",
|
||
"Flask-HTTPAuth, 206\n",
|
||
"before_request handler with authentication,\n",
|
||
"208\n",
|
||
"error handler, 208\n",
|
||
"initialization, 207\n",
|
||
"token-based authentication support, 209\n",
|
||
"Flask-Login, 107-115\n",
|
||
"adding a login form, 109\n",
|
||
"AnonymousUserMixin class, 132\n",
|
||
"current_user context variable, 113, 152\n",
|
||
"how it works, 113\n",
|
||
"LoginManager class, 108\n",
|
||
"login_manager.anonymous_user attribute,\n",
|
||
"132\n",
|
||
"login_required decorator, 108, 114\n",
|
||
"login_user function, 111\n",
|
||
"logout_user function, 113\n",
|
||
"preparing User model for logins, 107\n",
|
||
"signing users in, 111\n",
|
||
"signing users out, 112\n",
|
||
"testing logins, 114\n",
|
||
"UserMixin class, 107\n",
|
||
"user_loader decorator, 108\n",
|
||
"Flask-Mail, 79-84\n",
|
||
"initialization, 80\n",
|
||
"integrating emails with the application, 81\n",
|
||
"sending asynchronous email, 83\n",
|
||
"sending email from Python shell, 81\n",
|
||
"sending email through Gmail, 80\n",
|
||
"SMTP server configuration keys, 79\n",
|
||
"Flask-Migrate, 73-77\n",
|
||
"adding more migrations, 76\n",
|
||
"considerations in changing database sche‐\n",
|
||
"mas, 74\n",
|
||
"creating or upgrading database tables in\n",
|
||
"large application, 96\n",
|
||
"initialization, 73\n",
|
||
"upgrading the database, 75\n",
|
||
"Flask-Moment, 38\n",
|
||
"format function, 40\n",
|
||
"fromNow function, 40\n",
|
||
"locale function, 41\n",
|
||
"Flask-PageDown, 162-164\n",
|
||
"initialization, 162\n",
|
||
"Markdown-enabled post form, 162\n",
|
||
"Flask-SQLAlchemy, 60\n",
|
||
"add session method, 67-68\n",
|
||
"column options, 63\n",
|
||
"create_all method, 66, 95\n",
|
||
"database management with, 61\n",
|
||
"delete session method, 68\n",
|
||
"drop_all method, 66\n",
|
||
"enabling recording of query statistics, 239\n",
|
||
"filter_by query filter, 71\n",
|
||
"first_or_404 query method, 139\n",
|
||
"get_debug_queries function, 237\n",
|
||
"get_or_404 convenience function, 145\n",
|
||
"models, 62\n",
|
||
"MySQL configuration, 61\n",
|
||
"paginate method, 158\n",
|
||
"pagination object attributes, 158\n",
|
||
"pagination object methods, 159\n",
|
||
"Postgres configuration, 61\n",
|
||
"query executors, 69\n",
|
||
"query filters, 69\n",
|
||
"Index \n",
|
||
"| \n",
|
||
"283\n",
|
||
"\n",
|
||
"query object (database), 68\n",
|
||
"query statistics recorded by, 238\n",
|
||
"querying followed posts using databse join,\n",
|
||
"182\n",
|
||
"SQLALCHEMY_DATABASE_URI configu‐\n",
|
||
"ration, 61\n",
|
||
"SQLALCHEMY_TRACK_MODIFICA‐\n",
|
||
"TIONS configuration, 61\n",
|
||
"SQLite configuration, 61\n",
|
||
"Flask-SSLify, 249\n",
|
||
"Flask-WTF, 43\n",
|
||
"BooleanField class, 109\n",
|
||
"configuration, 44\n",
|
||
"cross-site request forgery (CSRF), 44\n",
|
||
"DataRequired validator, 45, 109\n",
|
||
"disabling CSRF tokens in unit tests, 226\n",
|
||
"FlaskForm class, 44\n",
|
||
"form fields, 45\n",
|
||
"Length validator, 109\n",
|
||
"login form, 109\n",
|
||
"PasswordField class, 109\n",
|
||
"rendering, 47\n",
|
||
"StringField class, 45, 109\n",
|
||
"SubmitField class, 45, 109\n",
|
||
"using to render a form, 47\n",
|
||
"validate_on_submit method, 49, 111\n",
|
||
"validators, 46\n",
|
||
"FlaskForm class, 44\n",
|
||
"Flasky, Docker container image for, 260\n",
|
||
"flasky.py, 266, 272\n",
|
||
"coverage command, 222\n",
|
||
"deploy command, 241, 253, 255, 260\n",
|
||
"profile command, 239\n",
|
||
"FLASKY_ADMIN environment variable, 83,\n",
|
||
"131\n",
|
||
"FLASKY_COMMENTS_PER_PAGE configu‐\n",
|
||
"ration variable, 192\n",
|
||
"FLASKY_POSTS_PER_PAGE configuration\n",
|
||
"variable, 158\n",
|
||
"FLASK_APP environment variable, 10, 66, 93,\n",
|
||
"246\n",
|
||
"setting by default, 97\n",
|
||
"FLASK_CONFIG environment variable, 93,\n",
|
||
"247\n",
|
||
"FLASK_COVERAGE environment variable,\n",
|
||
"223\n",
|
||
"FLASK_DEBUG environment variable, 14, 93\n",
|
||
"setting by default, 97\n",
|
||
"followers, 171-187\n",
|
||
"database relationships and, 171-178\n",
|
||
"on the profile page, 178-180\n",
|
||
"showing followed posts on home page,\n",
|
||
"183-187\n",
|
||
"for loops, 28\n",
|
||
"foreign keys, 57, 64\n",
|
||
"form.hidden_tag element, 47\n",
|
||
"form.validate_on_submit method, 142\n",
|
||
"format function, 40\n",
|
||
"forms (see web forms)\n",
|
||
"functools package, 133\n",
|
||
"G\n",
|
||
"g context variable, 18, 21\n",
|
||
"GET request method (HTTP), 19\n",
|
||
"in redirects, 51\n",
|
||
"in RESTful APIs, 201\n",
|
||
"resource handlers for blog posts, 213\n",
|
||
"view function handling GET requests, 48\n",
|
||
"get_flashed_messages function, 54\n",
|
||
"Git\n",
|
||
"downloading example code, 2\n",
|
||
"server dedicated to Heroku application, 246\n",
|
||
"source code for code examples, xiii\n",
|
||
"uploading applications to Heroku server\n",
|
||
"with git push, 254\n",
|
||
"using with applications on Heroku, 244\n",
|
||
"Gmail, Flask-Mail cofiguration for, 80\n",
|
||
"Gravatar service, 146\n",
|
||
"Gunicorn web server, 251-252\n",
|
||
"H\n",
|
||
"hashes\n",
|
||
"MD5 hash for avatar URLs, 146\n",
|
||
"caching of, 148\n",
|
||
"password hashing, 102\n",
|
||
"using Werkzeug security module, 102\n",
|
||
"HEAD request method (HTTP), 19\n",
|
||
"help from Flask developer community, 276\n",
|
||
"Heroku platform, 244-255\n",
|
||
"adding a Procfile, 252\n",
|
||
"adding a top-level requirements file, 248\n",
|
||
"addons:create command, 246\n",
|
||
"CLI tool, 245\n",
|
||
"config command, 246\n",
|
||
"config:set command, 247\n",
|
||
"configuring email, 248\n",
|
||
"configuring logging, 247\n",
|
||
"create command, 246\n",
|
||
"284 \n",
|
||
"| \n",
|
||
"Index\n",
|
||
"\n",
|
||
"creating a Heroku account, 245\n",
|
||
"creating an application, 245\n",
|
||
"deploying application upgrades to, 255\n",
|
||
"deploying applications to, using git push,\n",
|
||
"254\n",
|
||
"enabling secure HTTP with Flask-SSLify,\n",
|
||
"249\n",
|
||
"login command, 245\n",
|
||
"logs ommand, 247\n",
|
||
"maintenance command, 255\n",
|
||
"provisioning a database, 246\n",
|
||
"reviewing application logs, 255\n",
|
||
"running a production web server, 251\n",
|
||
"testing with heroku local command, 253\n",
|
||
"hostname (database URLs), 61\n",
|
||
"HTML\n",
|
||
"Markdown-to-HTML converter, 164\n",
|
||
"rendering of forms to, 47-48\n",
|
||
"HTTP (secure), enabling with Flask-SSLify, 249\n",
|
||
"HTTP authentication, 206\n",
|
||
"HTTP methods, 19\n",
|
||
"and resource handlers for RESTful web ser‐\n",
|
||
"vice, 215\n",
|
||
"request methods in RESTful APIs, 201\n",
|
||
"HTTP status codes, 21\n",
|
||
"404 error, 33, 145\n",
|
||
"RESTful API error handler for 403 status\n",
|
||
"code, 206\n",
|
||
"returned by RESTful APIs, 204\n",
|
||
"HTTPBasicAuth class, 207\n",
|
||
"HTTPie, using to test web services, 217-218\n",
|
||
"I\n",
|
||
"images (container), 244\n",
|
||
"building a Docker container image, 257\n",
|
||
"cleaning up, 269\n",
|
||
"importing files, template macros, 29\n",
|
||
"included files, 29\n",
|
||
"inheritance in Jinja2 templates, 29, 31\n",
|
||
"init_app method, 88\n",
|
||
"insert_roles method, 131\n",
|
||
"integrated development environments (IDEs),\n",
|
||
"275\n",
|
||
"itsdangerous package, 119\n",
|
||
"generating confirmation tokens for user\n",
|
||
"accounts, 119\n",
|
||
"token-based authentication support, 209\n",
|
||
"J\n",
|
||
"JavaScript files (Bootstrap), 30, 33\n",
|
||
"JavaScript, moment.js library, 38\n",
|
||
"Jinja2 package, 1, 26-30\n",
|
||
"block directive, 29\n",
|
||
"control structures, 28\n",
|
||
"for directive, 28\n",
|
||
"macro directive, 29\n",
|
||
"extends directive, 30\n",
|
||
"import directive, 29, 47\n",
|
||
"include directive, 29\n",
|
||
"rendering templates, 26\n",
|
||
"safe filter, 28\n",
|
||
"set directive, 195\n",
|
||
"super macro, 30\n",
|
||
"variables, 27\n",
|
||
"filters for, 27\n",
|
||
"wtf.quick_form macro, 110\n",
|
||
"joins (database)\n",
|
||
"joined lazy argument for back references,\n",
|
||
"175\n",
|
||
"using in databse query of followed posts,\n",
|
||
"181\n",
|
||
"jQuery.js library, 39\n",
|
||
"JSON\n",
|
||
"serializing resources to and from, 210-213\n",
|
||
"use in RESTful web services, 202\n",
|
||
"JSON Web Signatures (JWSs), 119\n",
|
||
"jsonify helper function, 203\n",
|
||
"junction tables (see association tables (data‐\n",
|
||
"base))\n",
|
||
"K\n",
|
||
"key-value databases, 57\n",
|
||
"Kubernetes, 270\n",
|
||
"L\n",
|
||
"language codes, 41\n",
|
||
"links, 36\n",
|
||
"blog post editor, 168\n",
|
||
"moderate comments link in navigation bar,\n",
|
||
"193\n",
|
||
"permanent links to blog posts, 165-167\n",
|
||
"profile edit link, 142\n",
|
||
"profile edit link for administrator, 145\n",
|
||
"to blog post comments, 192\n",
|
||
"to user profile page in navigation bar, 140\n",
|
||
"locale function, 41\n",
|
||
"Index \n",
|
||
"| \n",
|
||
"285\n",
|
||
"\n",
|
||
"localization of dates and time, 38\n",
|
||
"logging\n",
|
||
"configuring on Heroku platform, 247\n",
|
||
"Docker configuration for, 258\n",
|
||
"docker logs command, 262\n",
|
||
"of errors during production, 242\n",
|
||
"reviewing application logs on Heroku plat‐\n",
|
||
"form, 255\n",
|
||
"setting up in traditional deployments, 272\n",
|
||
"login view function, 111\n",
|
||
"login_manager.anonymous_user attribute, 132\n",
|
||
"login_manager.user_loader decorator, 108\n",
|
||
"login_required decorator, 108, 114, 122, 208\n",
|
||
"M\n",
|
||
"macros, 29\n",
|
||
"MAIL_PASSWORD environment variable, 80\n",
|
||
"MAIL_USERNAME environment variable, 80\n",
|
||
"make_response function, 21, 185\n",
|
||
"many-to-many relationships, 65, 172\n",
|
||
"advanced, 174\n",
|
||
"followers helper methods, 176\n",
|
||
"implementation as one-to-many rela‐\n",
|
||
"tionships, 175\n",
|
||
"self-referential, 174\n",
|
||
"many-to-one relationships, 65\n",
|
||
"Markdown, 162-165\n",
|
||
"conversion to HTML on the server, 164\n",
|
||
"post form enabled for, 162\n",
|
||
"messages, flashing from forms, 53-55\n",
|
||
"methods argument, 48\n",
|
||
"microservices, 265\n",
|
||
"Microsoft Windows (see Windows systems)\n",
|
||
"migration scripts (database), 74\n",
|
||
"creating with flask db migrate, 75\n",
|
||
"model definition (database), 62-64\n",
|
||
"moment.js library, 38\n",
|
||
"formatting options for dates and time, 40\n",
|
||
"MySQL databases, 264-265, 271\n",
|
||
"N\n",
|
||
"NameForm class, 45\n",
|
||
"namespaces in blueprints, 92\n",
|
||
"NoSQL databases, 57-58\n",
|
||
"Flask support for, xi\n",
|
||
"SQL databases vs., 59\n",
|
||
"O\n",
|
||
"OAuth2 authentication, 80\n",
|
||
"object-document mappers (ODMs), 60\n",
|
||
"object-relational mappers (ORMs), 60\n",
|
||
"model, 62\n",
|
||
"one-to-many relationships, 58, 64\n",
|
||
"comments table to users table, 189\n",
|
||
"many-to-many relationship implemented\n",
|
||
"as, 175\n",
|
||
"querying, 70\n",
|
||
"one-to-one relationships, 65\n",
|
||
"OPTIONS request method (HTTP), 19\n",
|
||
"orchestration (container) with docker-\n",
|
||
"compose, 265-269\n",
|
||
"P\n",
|
||
"PageDown library, 162\n",
|
||
"pagination\n",
|
||
"of large resource collections, 216\n",
|
||
"of long blog post lists, 155-161\n",
|
||
"adding a pagination widget, 158-161\n",
|
||
"creating fake blog post data, 155\n",
|
||
"rendering in pages, 157\n",
|
||
"of user comments on posts, 192\n",
|
||
"PasswordField class, 109\n",
|
||
"passwords\n",
|
||
"password in database URLs, 61\n",
|
||
"security, 102-105\n",
|
||
"updates and resets for user accounts, 125\n",
|
||
"performance, 237-240\n",
|
||
"logging slow database performance, 237-239\n",
|
||
"source code profiling, 239-240\n",
|
||
"permissions, 127\n",
|
||
"comment moderation, 193\n",
|
||
"custom decorators checking, 132\n",
|
||
"evaluating for a user, 132\n",
|
||
"in user roles database, 128\n",
|
||
"constants for permissions, 128\n",
|
||
"methods for managing, 129\n",
|
||
"roles supported with permissions, 130\n",
|
||
"unit tests for, 134\n",
|
||
"permission_required decorator, 214\n",
|
||
"pip, 5\n",
|
||
"Platform as a Service (PaaS), 244\n",
|
||
"POST request method (HTTP), 43\n",
|
||
"in RESTful APIs, 201\n",
|
||
"redirect response to POST requests, 51\n",
|
||
"resource handler for blog posts, 214\n",
|
||
"view function handling POST requests, 48\n",
|
||
"286 \n",
|
||
"| \n",
|
||
"Index\n",
|
||
"\n",
|
||
"Post/Redirect/Get pattern, 52\n",
|
||
"logins and, 112\n",
|
||
"Postfix, 271\n",
|
||
"Postgres databases, 246, 271\n",
|
||
"presentation logic, 25\n",
|
||
"primary key (database), 57\n",
|
||
"primay key column, Flask-SQLAlchemy\n",
|
||
"requirement for, 64\n",
|
||
"Procfile (applications on Heroku), 252\n",
|
||
"profile-header CSS class, 147\n",
|
||
"profile-thumbnail CSS class, 147\n",
|
||
"profiles, 137-149\n",
|
||
"blog post author username and avatar, links\n",
|
||
"to profile page, 153\n",
|
||
"blog posts on profile pages, 154\n",
|
||
"creating user profile page, 138-141\n",
|
||
"editor for, 141-145\n",
|
||
"administrator-level editor, 143-145\n",
|
||
"user-level editor, 141\n",
|
||
"followers on the profile page, 178-180\n",
|
||
"information in, 137\n",
|
||
"user avatars, 146-149\n",
|
||
"profiling source code, 239\n",
|
||
"proxy servers, 250, 271\n",
|
||
"psycopg2 package, 248\n",
|
||
"PUT request method (HTTP), 201\n",
|
||
"PUT resource handler for blog posts, 215\n",
|
||
"pymysql package, 264\n",
|
||
"Python, 1\n",
|
||
"creating virtual environments with Python\n",
|
||
"2, 3\n",
|
||
"creating virtual environments with Python\n",
|
||
"3, 3\n",
|
||
"database frameworks, 59-60\n",
|
||
"database integration with Python shell, 72\n",
|
||
"installing packages with pip, 5\n",
|
||
"interpreter images on Docker Hub, 258\n",
|
||
"Python Package Index, 276\n",
|
||
"python-dotenv package, 272\n",
|
||
"Q\n",
|
||
"query object (database), 68\n",
|
||
"R\n",
|
||
"redirect function, 22, 53\n",
|
||
"redirects, 22, 51-53\n",
|
||
"SSL, 249\n",
|
||
"Regexp form field validator, 116\n",
|
||
"registration of new users, 115-118\n",
|
||
"adding user registration form, 115\n",
|
||
"registering users, 117\n",
|
||
"regressions, 221\n",
|
||
"relational databases, 57\n",
|
||
"(see also SQL databases)\n",
|
||
"Flask support for, xi\n",
|
||
"relationships (database), 57, 64-65, 171-178\n",
|
||
"dynamic relationships, 70\n",
|
||
"many-to-many, 172\n",
|
||
"advanced, 174\n",
|
||
"querying a one-to-many relationship, 70\n",
|
||
"self-referential, 174\n",
|
||
"SQLAlchemy options for, 65\n",
|
||
"relative URLs (in links), 37\n",
|
||
"REMEMBER_COOKIE_DURATION option,\n",
|
||
"111\n",
|
||
"remote procedure call (RPC) protocols, 199\n",
|
||
"rendering (templates), 25\n",
|
||
"render_template function, 27, 49, 53\n",
|
||
"Representational State Transfer (see REST)\n",
|
||
"request context, 18\n",
|
||
"request-response cycle, 17-22\n",
|
||
"application and request contexts, 17\n",
|
||
"HTTP request methods, 19\n",
|
||
"request and response bodies in RESTful\n",
|
||
"APIs, 201\n",
|
||
"request dispatching, 18\n",
|
||
"request hooks, 20\n",
|
||
"request methods in RESTful APIs, 201\n",
|
||
"request object, 19\n",
|
||
"requests, 8\n",
|
||
"response formats for RESTful API clients,\n",
|
||
"205\n",
|
||
"responses, 8, 21\n",
|
||
"response object, 21\n",
|
||
"requirements file, 93, 248\n",
|
||
"development requirements file, 156\n",
|
||
"top-level requirements file for Heroku plat‐\n",
|
||
"form, 248\n",
|
||
"resources\n",
|
||
"implementing endpoints for, 213-216\n",
|
||
"pagination of large collections, 216\n",
|
||
"serializing to and from JSON, 210-213\n",
|
||
"URLs for, 202\n",
|
||
"resources for working with Flask, 275-277\n",
|
||
"getting help, 276\n",
|
||
"getting involved with the Flask community,\n",
|
||
"277\n",
|
||
"Index \n",
|
||
"| \n",
|
||
"287\n",
|
||
"\n",
|
||
"integrated development environments\n",
|
||
"(IDEs), 275\n",
|
||
"REST, 199\n",
|
||
"defining characteristics for web services\n",
|
||
"architecture, 199\n",
|
||
"request and response bodies, 201\n",
|
||
"request methods, 201\n",
|
||
"resources, concept of, 200\n",
|
||
"versioning of web services, 202\n",
|
||
"RESTful web services with Flask, 203-218\n",
|
||
"creating an API blueprint, 203\n",
|
||
"error handling, 204\n",
|
||
"implementing resource endpoints, 213-216\n",
|
||
"pagination of large resource collections, 216\n",
|
||
"serializing resources to and from JSON,\n",
|
||
"210-213\n",
|
||
"testing with HTTPie, 217-218\n",
|
||
"token-based authentication, 208-210\n",
|
||
"user authentication with Flask-HTTPAuth,\n",
|
||
"206\n",
|
||
"reverse proxy servers, 250, 271\n",
|
||
"rich internet applications (RIAs), 199\n",
|
||
"rich text posts, using Markdown and Flask-\n",
|
||
"PageDown, 161-165\n",
|
||
"handling rich text on the server, 164\n",
|
||
"roles, 127-135\n",
|
||
"adding to development database in shell ses‐\n",
|
||
"sion, 134\n",
|
||
"assignment of, 131\n",
|
||
"database representation of, 127-131\n",
|
||
"method creating roles, 130\n",
|
||
"unit tests for, 134\n",
|
||
"verification of, 132-135\n",
|
||
"rolling back database sessions, 68\n",
|
||
"Ronacher, Armin, 1\n",
|
||
"route decorator, 133\n",
|
||
"routes, 8\n",
|
||
"access by authenticated users only, 108\n",
|
||
"authentication token generation, 210\n",
|
||
"blog post comments support, 191\n",
|
||
"comment moderation, 196\n",
|
||
"dynamic, 12\n",
|
||
"editor for blog posts, 167\n",
|
||
"follow route, 179\n",
|
||
"home page route with blog posts, 152\n",
|
||
"pagination support, 157\n",
|
||
"in authentication blueprint, 105\n",
|
||
"in blueprints, 91\n",
|
||
"permanent links to blog posts, 165\n",
|
||
"profile edit route, 141\n",
|
||
"profile edit route for administrators, 144\n",
|
||
"profile page route, 138\n",
|
||
"profile page route with blog posts, 154\n",
|
||
"registration route with confirmation email,\n",
|
||
"120\n",
|
||
"selection of all or followed posts, 184\n",
|
||
"sign out, 113\n",
|
||
"user registration, 117\n",
|
||
"routing subsystem, 1\n",
|
||
"rows (database), 57\n",
|
||
"deleting, 68\n",
|
||
"inserting, 66\n",
|
||
"modifying, 68\n",
|
||
"querying, 68\n",
|
||
"S\n",
|
||
"secret key, 44, 87, 119\n",
|
||
"for applications on Heroku platform, 247\n",
|
||
"secure HTTP, 249\n",
|
||
"SelectField class, 144\n",
|
||
"Selenium, end-to-end testing with, 230-234\n",
|
||
"self-referential relationships, 174\n",
|
||
"Sendmail, 271\n",
|
||
"serialization\n",
|
||
"deserializing resources from JSON, 212\n",
|
||
"serializing resources to JSON, 210\n",
|
||
"server setup in traditional deployment, 271\n",
|
||
"server shutdown, 230\n",
|
||
"session context variable, 18, 52\n",
|
||
"session.get method, 53\n",
|
||
"sessions (database), 67\n",
|
||
"committing, 67\n",
|
||
"rolling back, 68\n",
|
||
"sessions (user), 44\n",
|
||
"redirects and, 51\n",
|
||
"set_cookie method, 21, 185\n",
|
||
"shell context processor, 72\n",
|
||
"SMTP server, 79\n",
|
||
"smtplib package, 79\n",
|
||
"source code profiling, 239\n",
|
||
"SQL (Structured Query Language), 57\n",
|
||
"SQL databases, 57\n",
|
||
"NoSQL vs., 59\n",
|
||
"SQLAlchemy, 60\n",
|
||
"column options, 63\n",
|
||
"column types, 63\n",
|
||
"inspecting native SQL query generated by,\n",
|
||
"69\n",
|
||
"288 \n",
|
||
"| \n",
|
||
"Index\n",
|
||
"\n",
|
||
"Markdown text conversion to HTML, 164\n",
|
||
"query executors, 69\n",
|
||
"query filters, 69\n",
|
||
"relationship options, 65\n",
|
||
"SQLALCHEMY_DATABASE_URI, 61, 87\n",
|
||
"SQLALCHEMY_TRACK_MODIFICATIONS,\n",
|
||
"61\n",
|
||
"SQLite databases, 271\n",
|
||
"SSL_REDIRECT variable, 249, 250\n",
|
||
"stack traces, 242\n",
|
||
"stateless web services, 206\n",
|
||
"static files, 37\n",
|
||
"static methods, 131\n",
|
||
"status codes (HTTP), 21\n",
|
||
"StringField class, 45, 109\n",
|
||
"SubmitField class, 45, 109\n",
|
||
"super function, 30, 33\n",
|
||
"syslog, 272\n",
|
||
"T\n",
|
||
"__tablename__ class variable, 62\n",
|
||
"tables (database), 57\n",
|
||
"creating, 66\n",
|
||
"creating or upgrading in large application,\n",
|
||
"96\n",
|
||
"joins, 58\n",
|
||
"teardown_request hook, 20\n",
|
||
"templates, 25-41\n",
|
||
"adding Permission class to template context,\n",
|
||
"134\n",
|
||
"Bootstrap integration using Flash-\n",
|
||
"Bootstrap, 30-33\n",
|
||
"comment moderation, 194\n",
|
||
"confirmation email used by authentication\n",
|
||
"blueprint, 121\n",
|
||
"custom error pages, 33-36\n",
|
||
"defined, 25\n",
|
||
"edit blog post template, 167\n",
|
||
"flash message rendering, 54\n",
|
||
"Flask-PageDown template declaration, 162\n",
|
||
"follower enhancements to profile header,\n",
|
||
"178\n",
|
||
"for authentication blueprint, 105\n",
|
||
"for email messages, 81\n",
|
||
"for login form, 109\n",
|
||
"for new user registration form, 116\n",
|
||
"for user profile, 139\n",
|
||
"greeting logged-in user, 114\n",
|
||
"home page template with blog posts, 152\n",
|
||
"Jinja2 template engine, 26-30\n",
|
||
"links, 36\n",
|
||
"localizing dates and time with Flask-\n",
|
||
"Moment, 38\n",
|
||
"login template, updating to render login\n",
|
||
"form, 112\n",
|
||
"pagination footer for blog post lists, 160\n",
|
||
"pagination template macro, 159\n",
|
||
"permanent link to blog posts, 166\n",
|
||
"profile page template with blog posts, 155\n",
|
||
"static files, 37\n",
|
||
"templates folder, 26\n",
|
||
"using to render a form to HTML, 47\n",
|
||
"test command to run unit tests, 95\n",
|
||
"testing, 221-235\n",
|
||
"assessing worth of, 234\n",
|
||
"end-to-end, using Selenium, 230-234\n",
|
||
"getting code coverage reports, 221-224\n",
|
||
"of web services with HTTPie, 217-218\n",
|
||
"password hashing tests, 104\n",
|
||
"unit tests file for large applications, 94\n",
|
||
"unit tests for roles and permissions, 134\n",
|
||
"using Flask test client, 224-229\n",
|
||
"testing web applications, 225-228\n",
|
||
"testing web services, 228-229\n",
|
||
"verifying login functionality, 114\n",
|
||
"time (see dates and time)\n",
|
||
"timestamps, working with, using Flask-\n",
|
||
"Moment, 39\n",
|
||
"token-based authentication, 208-210, 218\n",
|
||
"transactions, 67\n",
|
||
"(see also sessions (database))\n",
|
||
"U\n",
|
||
"unconfirmed accounts\n",
|
||
"filtering in before_app_request handler, 122\n",
|
||
"page presented asking for account confir‐\n",
|
||
"mation, 123\n",
|
||
"unittest package, 95\n",
|
||
"URL fragments, 192\n",
|
||
"URLs\n",
|
||
"application routes, 9\n",
|
||
"application URL map, 19\n",
|
||
"avatar, 146\n",
|
||
"database, in Flask-SQLAlchemy, 61\n",
|
||
"for resources, 200\n",
|
||
"for resoures\n",
|
||
"fully-qualified, 202\n",
|
||
"Index \n",
|
||
"| \n",
|
||
"289\n",
|
||
"\n",
|
||
"in confirmation emails for user accounts,\n",
|
||
"121\n",
|
||
"in links, 37\n",
|
||
"url_for function, 37, 53, 92, 121\n",
|
||
"url_prefix argument, 106\n",
|
||
"user authentication (see authentication)\n",
|
||
"user comments, 189-197\n",
|
||
"database representation of, 189-191\n",
|
||
"moderation of, 193-197\n",
|
||
"submission and display of, 191-193\n",
|
||
"user loader function, 108\n",
|
||
"User model\n",
|
||
"preparing for logins, 107\n",
|
||
"preparing for password hashing, 102\n",
|
||
"user account token generation and verifica‐\n",
|
||
"tion, 119\n",
|
||
"user profiles (see profiles)\n",
|
||
"user roles (see roles)\n",
|
||
"user sessions, 44, 52\n",
|
||
"expiration, long-term cookie for, 111\n",
|
||
"User.can method, 153\n",
|
||
"username (database URLs), 61\n",
|
||
"users\n",
|
||
"making existing users their own followers,\n",
|
||
"186\n",
|
||
"making their own followers on construc‐\n",
|
||
"tion, 186\n",
|
||
"UTC (Coordinated Universal Time), 38\n",
|
||
"uWSGI web server, 252\n",
|
||
"V\n",
|
||
"validate_on_submit method, 49, 111\n",
|
||
"ValidationError, 116, 212\n",
|
||
"RESTful API error handler for, 213\n",
|
||
"validators, 44, 109\n",
|
||
"built-in, WTForms package, 46\n",
|
||
"implemented as methods, 116\n",
|
||
"variables, 26-27\n",
|
||
"filters for, 27\n",
|
||
"venv package (Python), 3\n",
|
||
"verify_password method, 111\n",
|
||
"view functions, 8\n",
|
||
"confirming user accounts, 121\n",
|
||
"database use in, 71-72\n",
|
||
"for blog post comments, 191\n",
|
||
"for blog post editor, 168\n",
|
||
"for followers on profile page, 179\n",
|
||
"for permanent links to blog posts, 165\n",
|
||
"form handling in, 48-51\n",
|
||
"in authentication blueprint, 105\n",
|
||
"in blueprints, 92\n",
|
||
"login function implementation, 111\n",
|
||
"order of decorators in, 133\n",
|
||
"purposes of, 25\n",
|
||
"virtual environments, 2\n",
|
||
"activating, 4\n",
|
||
"creating with Python 2, 3\n",
|
||
"creating with Python 3, 3\n",
|
||
"deactivating, 5\n",
|
||
"installing Flask into, 5\n",
|
||
"using without activating, 5\n",
|
||
"virtual machines, 256\n",
|
||
"virtual servers, 243\n",
|
||
"virtualenv utility, 3\n",
|
||
"volumes (Docker), removing, 269\n",
|
||
"W\n",
|
||
"Waitress web server, 252\n",
|
||
"web browsers, Selenium automation tool, 230\n",
|
||
"web dynos (Heroku), 244\n",
|
||
"web forms, 43-55\n",
|
||
"blog post form, 151\n",
|
||
"configuration, 44\n",
|
||
"flashing a message, 53-55\n",
|
||
"form classes, 44-47\n",
|
||
"generated by Flash-WTF, CSRF tokens in,\n",
|
||
"226\n",
|
||
"handling in view functions, 48-51\n",
|
||
"HTML rendering of, 47-48\n",
|
||
"in blueprints, 92\n",
|
||
"login form, 109\n",
|
||
"profile edit form, 141\n",
|
||
"profile editing form for administrators, 143\n",
|
||
"redirects and user sessions, 51-53\n",
|
||
"user registration form, 115\n",
|
||
"Web Server Gateway Interface (WSGI), 1, 7\n",
|
||
"web servers\n",
|
||
"installing in traditional deployment, 271\n",
|
||
"running production web server on Heroku,\n",
|
||
"251\n",
|
||
"web services, 199, 203\n",
|
||
"(see also RESTful web services with Flask)\n",
|
||
"testing RESTful web services using Flask test\n",
|
||
"client, 228-229\n",
|
||
"Werkzeug (security module), 1, 102, 242\n",
|
||
"generate_password_hash function, 103\n",
|
||
"ProfilerMiddleware WSGI middleware, 240\n",
|
||
"ProxyFix WSGI middleware, 250\n",
|
||
"290 \n",
|
||
"| \n",
|
||
"Index\n",
|
||
"\n",
|
||
"verify_password method, 103\n",
|
||
"Windows Subsystem for Linux (WSL), 2\n",
|
||
"Windows systems, 2\n",
|
||
"Docker for Windows, Hyper-V feature and,\n",
|
||
"257\n",
|
||
"testing Heroku deployment, using Waitress\n",
|
||
"web server, 252\n",
|
||
"worker dynos (Heroku), 244\n",
|
||
"wtf.quick_form function, 47\n",
|
||
"WTForms package, 43\n",
|
||
"built-in validators, 46\n",
|
||
"Regexp validator, 116\n",
|
||
"SelectField wrapper class, 144\n",
|
||
"standard HTML fields supported by, 45\n",
|
||
"X\n",
|
||
"XML in RESTful web services, 202\n",
|
||
"Index \n",
|
||
"| \n",
|
||
"291\n",
|
||
"\n",
|
||
"About the Author\n",
|
||
"Miguel Grinberg has over 25 years of experience as a software engineer. He has a\n",
|
||
"blog where he writes about a variety of topics, including web development, robotics,\n",
|
||
"photography, and the occasional movie review. He lives in Portland, Oregon.\n",
|
||
"Colophon\n",
|
||
"The animal on the cover of Flask Web Development is a Pyrenean Mastiff (a breed of\n",
|
||
"Canis lupus familiaris). These giant Spanish dogs are descended from an ancient live‐\n",
|
||
"stock guardian dog called the Molossus, which was bred by the Greeks and Romans\n",
|
||
"and is now extinct. However, this ancestor is known to have played a role in the cre‐\n",
|
||
"ation of many breeds that are common today, such as the Rottweiler, Great Dane,\n",
|
||
"Newfoundland, and Cane Corso. Pyrenean Mastiffs have only been recognized as a\n",
|
||
"pure breed since 1977, and the Pyrenean Mastiff Club of America is working to pro‐\n",
|
||
"mote these dogs as pets in the United States.\n",
|
||
"After the Spanish Civil War, the population of Pyrenean Mastiffs in their native\n",
|
||
"homeland plummeted, and the breed only survived due to the dedicated work of a\n",
|
||
"few scattered breeders throughout the country. The modern gene pool for Pyreneans\n",
|
||
"stems from this postwar population, making them prone to genetic diseases like hip\n",
|
||
"dysplasia. Today, responsible owners make sure their dogs are tested for diseases and\n",
|
||
"x-rayed to look for hip abnormalities before being bred.\n",
|
||
"Adult male Pyrenean Mastiffs can reach upwards of 200 pounds when fully grown, so\n",
|
||
"owning this dog requires a commitment to good training and plenty of outdoor time.\n",
|
||
"Despite their size and history as hunters of bears and wolves, the Pyrenean has a very\n",
|
||
"calm temperament and is an excellent family dog. They can be relied upon to take\n",
|
||
"care of children and protect the home, while at the same time being docile with other\n",
|
||
"dogs. With proper socialization and strong leadership, the Pyrenean Mastiff thrives in\n",
|
||
"a home environment and will provide an excellent guardian and companion.\n",
|
||
"Many of the animals on O’Reilly covers are endangered; all of them are important to\n",
|
||
"the world. To learn more about how you can help, go to animals.oreilly.com.\n",
|
||
"The cover image is from J. G. Wood’s Animate Creation. The cover fonts are URW\n",
|
||
"Typewriter and Guardian Sans. The text font is Adobe Minion Pro; the heading font\n",
|
||
"is Adobe Myriad Condensed; and the code font is Dalton Maag’s Ubuntu Mono.\n",
|
||
"\n",
|
||
"Text Statistics:\n",
|
||
"Word Count: 98815\n",
|
||
"Sentence Count: 4310\n",
|
||
"Average Word Length: 4.45\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import fitz # PyMuPDF\n",
|
||
"import nltk\n",
|
||
"from nltk.tokenize import word_tokenize, sent_tokenize\n",
|
||
"\n",
|
||
"# Download necessary NLTK data files\n",
|
||
"nltk.download('punkt')\n",
|
||
"\n",
|
||
"def extract_text_from_pdf(pdf_path):\n",
|
||
" pdf_document = fitz.open(pdf_path)\n",
|
||
" text_content = []\n",
|
||
"\n",
|
||
" for page_num in range(pdf_document.page_count):\n",
|
||
" page = pdf_document.load_page(page_num)\n",
|
||
" text_content.append(page.get_text())\n",
|
||
"\n",
|
||
" return '\\n'.join(text_content)\n",
|
||
"\n",
|
||
"def text_statistics(text):\n",
|
||
" words = word_tokenize(text)\n",
|
||
" sentences = sent_tokenize(text)\n",
|
||
" word_count = len(words)\n",
|
||
" sentence_count = len(sentences)\n",
|
||
" average_word_length = sum(len(word) for word in words) / word_count\n",
|
||
"\n",
|
||
" return {\n",
|
||
" 'word_count': word_count,\n",
|
||
" 'sentence_count': sentence_count,\n",
|
||
" 'average_word_length': average_word_length\n",
|
||
" }\n",
|
||
"\n",
|
||
"# Example usage\n",
|
||
"fname = '/home/ys/Documents/Flask Web Development - Miguel Grinberg.pdf'\n",
|
||
"text = extract_text_from_pdf(fname)\n",
|
||
"print(text)\n",
|
||
"\n",
|
||
"# Generate text statistics\n",
|
||
"stats = text_statistics(text)\n",
|
||
"print(\"Text Statistics:\")\n",
|
||
"print(f\"Word Count: {stats['word_count']}\")\n",
|
||
"print(f\"Sentence Count: {stats['sentence_count']}\")\n",
|
||
"print(f\"Average Word Length: {stats['average_word_length']:.2f}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": []
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "General",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.11.2"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 4
|
||
}
|