We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
qa_loader.py uses a fixed random seed every run
## Description

Always using the same seed makes it impossible to run `qa_loader.py` multiple times with the same database, as supposedly random values aren't, causing unique constraint violations.

## Steps to Reproduce

- Run the dev server with `make dev`
- Start a shell in the container with `docker exec -it securedrop-dev-0 bash`
- In that shell, run `./qa_loader --journalist-count 1 --source-count 1`
- Run the same command a second time.

## Expected Behavior

That you could keep adding random journalists and sources to the database.

## Actual Behavior

You get `sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: journalists.username` because [`random.seed` is always called with the same value](https://github.com/freedomofpress/securedrop/blob/ec2220c3c2b9120d029b616d3a07647b175bc6ab/securedrop/qa_loader.py#L22).

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/qa_loader.py`
Content:
```
1 #!/opt/venvs/securedrop-app-code/bin/python
2 # -*- coding: utf-8 -*-
3 
4 import os
5 import random
6 import string
7 import sys
8 from argparse import ArgumentParser
9 from datetime import datetime
10 from itertools import cycle
11 from os import path
12 
13 from flask import current_app
14 
15 from crypto_util import DICEWARE_SAFE_CHARS
16 from db import db
17 from journalist_app import create_app
18 from models import Journalist, JournalistLoginAttempt, Reply, Source, SourceStar, Submission
19 from sdconfig import config as sdconfig
20 
21 
22 random.seed("~(=^–^)")  # mrow?
23 
24 
25 def random_bool():
26     return bool(random.getrandbits(1))
27 
28 
29 def random_chars(len, nullable, chars=string.ascii_letters):
30     if nullable and random_bool():
31         return None
32     else:
33         return "".join([random.choice(chars) for _ in range(len)])
34 
35 
36 def bool_or_none():
37     return random.choice([True, False, None])
38 
39 
40 def random_datetime(nullable):
41     if nullable and random_bool():
42         return None
43     else:
44         now = datetime.now()
45         return datetime(
46             year=random.randint(2013, now.year),
47             month=random.randint(1, now.month),
48             day=random.randint(1, now.day),
49             hour=random.randint(0, 23),
50             minute=random.randint(0, 59),
51             second=random.randint(0, 59),
52             microsecond=random.randint(0, 1000),
53         )
54 
55 
56 def positive_int(s):
57     i = int(s)
58     if i < 1:
59         raise ValueError("{} is not >= 1".format(s))
60     return i
61 
62 
63 def fraction(s):
64     f = float(s)
65     if 0 <= f <= 1:
66         return f
67     raise ValueError("{} should be a float between 0 and 1".format(s))
68 
69 
70 submissions = cycle(
71     [
72         "This is a test submission without markup!",
73         'This is a test submission with markup and characters such as \, \\, \', " and ". '
74         + "<strong>This text should not be bold</strong>!",  # noqa: W605, E501
75     ]
76 )
77 
78 
79 replies = cycle(
80     [
81         "This is a test reply without markup!",
82         'This is a test reply with markup and characters such as \, \\, \', " and ". '
83         + "<strong>This text should not be bold</strong>!",  # noqa: W605, E501
84     ]
85 )
86 
87 
88 class QaLoader(object):
89     def __init__(
90         self,
91         config,
92         journalist_count=10,
93         source_count=50,
94         submissions_per_source=1,
95         replies_per_source=1,
96         source_star_fraction=0.1,
97         source_reply_fraction=0.5,
98     ):
99         """
100         source_star_fraction and source_reply_fraction are simply the
101         fraction of sources starred or replied to.
102         """
103         self.config = config
104         self.app = create_app(config)
105 
106         self.journalist_count = journalist_count
107         self.source_count = source_count
108         self.submissions_per_source = submissions_per_source
109         self.replies_per_source = replies_per_source
110         self.source_star_fraction = source_star_fraction
111         self.source_reply_fraction = source_reply_fraction
112 
113         self.journalists = []
114         self.sources = []
115 
116     def new_journalist(self):
117         # Make a diceware-like password
118         pw = " ".join(
119             [random_chars(3, nullable=False, chars=DICEWARE_SAFE_CHARS) for _ in range(7)]
120         )
121         journalist = Journalist(
122             username=random_chars(random.randint(3, 32), nullable=False),
123             password=pw,
124             is_admin=random_bool(),
125         )
126         if random_bool():
127             # to add legacy passwords back in
128             journalist.passphrase_hash = None
129             journalist.pw_salt = random_chars(32, nullable=False).encode("utf-8")
130             journalist.pw_hash = random_chars(64, nullable=False).encode("utf-8")
131 
132         journalist.is_admin = bool_or_none()
133 
134         journalist.is_totp = bool_or_none()
135         journalist.hotp_counter = random.randint(-1000, 1000) if random_bool() else None
136         journalist.created_on = random_datetime(nullable=True)
137         journalist.last_access = random_datetime(nullable=True)
138 
139         db.session.add(journalist)
140         db.session.flush()
141         self.journalists.append(journalist.id)
142 
143     def new_source(self):
144         codename = current_app.crypto_util.genrandomid()
145         filesystem_id = current_app.crypto_util.hash_codename(codename)
146         journalist_designation = current_app.crypto_util.display_id()
147         source = Source(filesystem_id, journalist_designation)
148         db.session.add(source)
149         db.session.flush()
150 
151         # Generate submissions directory and generate source key
152         os.mkdir(current_app.storage.path(source.filesystem_id))
153         current_app.crypto_util.genkeypair(source.filesystem_id, codename)
154 
155         self.sources.append(source.id)
156 
157     def new_submission(self, source_id):
158         source = Source.query.get(source_id)
159 
160         source.interaction_count += 1
161         fpath = current_app.storage.save_message_submission(
162             source.filesystem_id,
163             source.interaction_count,
164             source.journalist_filename,
165             next(submissions),
166         )
167         submission = Submission(source, fpath)
168         db.session.add(submission)
169 
170         source.pending = False
171         source.last_updated = datetime.utcnow()
172 
173         db.session.flush()
174 
175     def new_source_star(self, source_id):
176         source = Source.query.get(source_id)
177         star = SourceStar(source, bool_or_none())
178         db.session.add(star)
179 
180     def new_reply(self, journalist_id, source_id):
181         source = Source.query.get(source_id)
182 
183         journalist = Journalist.query.get(journalist_id)
184 
185         source.interaction_count += 1
186         source.last_updated = datetime.utcnow()
187 
188         fname = "{}-{}-reply.gpg".format(source.interaction_count, source.journalist_filename)
189         current_app.crypto_util.encrypt(
190             next(replies),
191             [
192                 current_app.crypto_util.get_fingerprint(source.filesystem_id),
193                 sdconfig.JOURNALIST_KEY
194             ],
195             current_app.storage.path(source.filesystem_id, fname),
196         )
197 
198         reply = Reply(journalist, source, fname)
199         db.session.add(reply)
200         db.session.flush()
201 
202     def new_journalist_login_attempt(self, journalist_id):
203         journalist = Journalist.query.get(journalist_id)
204         attempt = JournalistLoginAttempt(journalist)
205         attempt.timestamp = random_datetime(nullable=True)
206         db.session.add(attempt)
207 
208     def load(self):
209         with self.app.app_context():
210             print("Creating {:d} journalists...".format(self.journalist_count))
211             for i in range(1, self.journalist_count + 1):
212                 self.new_journalist()
213                 if i % min(10, max(1, int(self.journalist_count / 10))) == 0:
214                     sys.stdout.write("{}\r{}".format(" " * len(str(self.journalist_count + 1)), i))
215             print("\n")
216             db.session.commit()
217 
218             print("Creating {:d} sources...".format(self.source_count))
219             for i in range(1, self.source_count + 1):
220                 self.new_source()
221                 if i % min(10, max(1, int(self.source_count / 10))) == 0:
222                     sys.stdout.write("{}\r{}".format(" " * len(str(self.source_count + 1)), i))
223             print("\n")
224             db.session.commit()
225 
226             print(
227                 "Creating submissions ({:d} each) for each source...".format(
228                     self.submissions_per_source
229                 )
230             )
231             for sid in self.sources:
232                 for _ in range(1, self.submissions_per_source + 1):
233                     self.new_submission(sid)
234             db.session.commit()
235 
236             print("Starring {:.2f}% of all sources...".format(self.source_star_fraction * 100))
237             for sid in random.sample(
238                 self.sources, int(self.source_count * self.source_star_fraction)
239             ):
240                 self.new_source_star(sid)
241             db.session.commit()
242 
243             print(
244                 "Creating replies ({:d} each) for {:.2f}% of sources...".format(
245                     self.replies_per_source, self.source_reply_fraction * 100
246                 )
247             )
248             for sid in random.sample(
249                 self.sources, int(self.source_count * self.source_reply_fraction)
250             ):
251                 jid = random.choice(self.journalists)
252                 for _ in range(self.replies_per_source):
253                     self.new_reply(jid, sid)
254             db.session.commit()
255 
256             for jid in self.journalists:
257                 self.new_journalist_login_attempt(jid)
258             db.session.commit()
259 
260 
261 def arg_parser():
262     parser = ArgumentParser(
263         path.basename(__file__), description="Loads data into the database for testing upgrades"
264     )
265     parser.add_argument(
266         "--journalist-count",
267         type=positive_int,
268         default=10,
269         help=("Number of journalists to create"),
270     )
271     parser.add_argument(
272         "--source-count", type=positive_int, default=50, help=("Number of sources to create")
273     )
274     parser.add_argument(
275         "--submissions-per-source",
276         type=positive_int,
277         default=1,
278         help=("Number of submissions to create for each source"),
279     )
280     parser.add_argument(
281         "--replies-per-source",
282         type=positive_int,
283         default=1,
284         help=("Number of replies to create for each source"),
285     )
286     parser.add_argument(
287         "--source-star-fraction",
288         type=fraction,
289         default=0.1,
290         help=("Fraction of sources to star"),
291     )
292     parser.add_argument(
293         "--source-reply-fraction",
294         type=fraction,
295         default=0.5,
296         help=("Fraction of sources to reply to"),
297     )
298     return parser
299 
300 
301 def main():
302     args = arg_parser().parse_args()
303     print("Loading data. This may take a while.")
304     QaLoader(
305         sdconfig,
306         args.journalist_count,
307         args.source_count,
308         args.submissions_per_source,
309         args.replies_per_source,
310         args.source_star_fraction,
311         args.source_reply_fraction,
312     ).load()
313 
314 
315 if __name__ == "__main__":
316     try:
317         main()
318     except KeyboardInterrupt:
319         print("")  # for prompt on a newline
320         sys.exit(1)
321
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/qa_loader.py b/securedrop/qa_loader.py
--- a/securedrop/qa_loader.py
+++ b/securedrop/qa_loader.py
@@ -19,9 +19,6 @@
 from sdconfig import config as sdconfig
 
 
-random.seed("~(=^–^)")  # mrow?
-
-
 def random_bool():
     return bool(random.getrandbits(1))

{"golden_diff": "diff --git a/securedrop/qa_loader.py b/securedrop/qa_loader.py\n--- a/securedrop/qa_loader.py\n+++ b/securedrop/qa_loader.py\n@@ -19,9 +19,6 @@\n from sdconfig import config as sdconfig\n \n \n-random.seed(\"~(=^\u2013^)\")  # mrow?\n-\n-\n def random_bool():\n     return bool(random.getrandbits(1))\n", "issue": "qa_loader.py uses a fixed random seed every run\n## Description\r\n\r\nAlways using the same seed makes it impossible to run `qa_loader.py` multiple times with the same database, as supposedly random values aren't, causing unique constraint violations.\r\n\r\n## Steps to Reproduce\r\n\r\n- Run the dev server with `make dev`\r\n- Start a shell in the container with `docker exec -it securedrop-dev-0 bash`\r\n- In that shell, run `./qa_loader --journalist-count 1 --source-count 1`\r\n- Run the same command a second time.\r\n\r\n## Expected Behavior\r\n\r\nThat you could keep adding random journalists and sources to the database.\r\n\r\n## Actual Behavior\r\n\r\nYou get `sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: journalists.username` because [`random.seed` is always called with the same value](https://github.com/freedomofpress/securedrop/blob/ec2220c3c2b9120d029b616d3a07647b175bc6ab/securedrop/qa_loader.py#L22).\n", "before_files": [{"content": "#!/opt/venvs/securedrop-app-code/bin/python\n# -*- coding: utf-8 -*-\n\nimport os\nimport random\nimport string\nimport sys\nfrom argparse import ArgumentParser\nfrom datetime import datetime\nfrom itertools import cycle\nfrom os import path\n\nfrom flask import current_app\n\nfrom crypto_util import DICEWARE_SAFE_CHARS\nfrom db import db\nfrom journalist_app import create_app\nfrom models import Journalist, JournalistLoginAttempt, Reply, Source, SourceStar, Submission\nfrom sdconfig import config as sdconfig\n\n\nrandom.seed(\"~(=^\u2013^)\")  # mrow?\n\n\ndef random_bool():\n    return bool(random.getrandbits(1))\n\n\ndef random_chars(len, nullable, chars=string.ascii_letters):\n    if nullable and random_bool():\n        return None\n    else:\n        return \"\".join([random.choice(chars) for _ in range(len)])\n\n\ndef bool_or_none():\n    return random.choice([True, False, None])\n\n\ndef random_datetime(nullable):\n    if nullable and random_bool():\n        return None\n    else:\n        now = datetime.now()\n        return datetime(\n            year=random.randint(2013, now.year),\n            month=random.randint(1, now.month),\n            day=random.randint(1, now.day),\n            hour=random.randint(0, 23),\n            minute=random.randint(0, 59),\n            second=random.randint(0, 59),\n            microsecond=random.randint(0, 1000),\n        )\n\n\ndef positive_int(s):\n    i = int(s)\n    if i < 1:\n        raise ValueError(\"{} is not >= 1\".format(s))\n    return i\n\n\ndef fraction(s):\n    f = float(s)\n    if 0 <= f <= 1:\n        return f\n    raise ValueError(\"{} should be a float between 0 and 1\".format(s))\n\n\nsubmissions = cycle(\n    [\n        \"This is a test submission without markup!\",\n        'This is a test submission with markup and characters such as \\, \\\\, \\', \" and \". '\n        + \"<strong>This text should not be bold</strong>!\",  # noqa: W605, E501\n    ]\n)\n\n\nreplies = cycle(\n    [\n        \"This is a test reply without markup!\",\n        'This is a test reply with markup and characters such as \\, \\\\, \\', \" and \". '\n        + \"<strong>This text should not be bold</strong>!\",  # noqa: W605, E501\n    ]\n)\n\n\nclass QaLoader(object):\n    def __init__(\n        self,\n        config,\n        journalist_count=10,\n        source_count=50,\n        submissions_per_source=1,\n        replies_per_source=1,\n        source_star_fraction=0.1,\n        source_reply_fraction=0.5,\n    ):\n        \"\"\"\n        source_star_fraction and source_reply_fraction are simply the\n        fraction of sources starred or replied to.\n        \"\"\"\n        self.config = config\n        self.app = create_app(config)\n\n        self.journalist_count = journalist_count\n        self.source_count = source_count\n        self.submissions_per_source = submissions_per_source\n        self.replies_per_source = replies_per_source\n        self.source_star_fraction = source_star_fraction\n        self.source_reply_fraction = source_reply_fraction\n\n        self.journalists = []\n        self.sources = []\n\n    def new_journalist(self):\n        # Make a diceware-like password\n        pw = \" \".join(\n            [random_chars(3, nullable=False, chars=DICEWARE_SAFE_CHARS) for _ in range(7)]\n        )\n        journalist = Journalist(\n            username=random_chars(random.randint(3, 32), nullable=False),\n            password=pw,\n            is_admin=random_bool(),\n        )\n        if random_bool():\n            # to add legacy passwords back in\n            journalist.passphrase_hash = None\n            journalist.pw_salt = random_chars(32, nullable=False).encode(\"utf-8\")\n            journalist.pw_hash = random_chars(64, nullable=False).encode(\"utf-8\")\n\n        journalist.is_admin = bool_or_none()\n\n        journalist.is_totp = bool_or_none()\n        journalist.hotp_counter = random.randint(-1000, 1000) if random_bool() else None\n        journalist.created_on = random_datetime(nullable=True)\n        journalist.last_access = random_datetime(nullable=True)\n\n        db.session.add(journalist)\n        db.session.flush()\n        self.journalists.append(journalist.id)\n\n    def new_source(self):\n        codename = current_app.crypto_util.genrandomid()\n        filesystem_id = current_app.crypto_util.hash_codename(codename)\n        journalist_designation = current_app.crypto_util.display_id()\n        source = Source(filesystem_id, journalist_designation)\n        db.session.add(source)\n        db.session.flush()\n\n        # Generate submissions directory and generate source key\n        os.mkdir(current_app.storage.path(source.filesystem_id))\n        current_app.crypto_util.genkeypair(source.filesystem_id, codename)\n\n        self.sources.append(source.id)\n\n    def new_submission(self, source_id):\n        source = Source.query.get(source_id)\n\n        source.interaction_count += 1\n        fpath = current_app.storage.save_message_submission(\n            source.filesystem_id,\n            source.interaction_count,\n            source.journalist_filename,\n            next(submissions),\n        )\n        submission = Submission(source, fpath)\n        db.session.add(submission)\n\n        source.pending = False\n        source.last_updated = datetime.utcnow()\n\n        db.session.flush()\n\n    def new_source_star(self, source_id):\n        source = Source.query.get(source_id)\n        star = SourceStar(source, bool_or_none())\n        db.session.add(star)\n\n    def new_reply(self, journalist_id, source_id):\n        source = Source.query.get(source_id)\n\n        journalist = Journalist.query.get(journalist_id)\n\n        source.interaction_count += 1\n        source.last_updated = datetime.utcnow()\n\n        fname = \"{}-{}-reply.gpg\".format(source.interaction_count, source.journalist_filename)\n        current_app.crypto_util.encrypt(\n            next(replies),\n            [\n                current_app.crypto_util.get_fingerprint(source.filesystem_id),\n                sdconfig.JOURNALIST_KEY\n            ],\n            current_app.storage.path(source.filesystem_id, fname),\n        )\n\n        reply = Reply(journalist, source, fname)\n        db.session.add(reply)\n        db.session.flush()\n\n    def new_journalist_login_attempt(self, journalist_id):\n        journalist = Journalist.query.get(journalist_id)\n        attempt = JournalistLoginAttempt(journalist)\n        attempt.timestamp = random_datetime(nullable=True)\n        db.session.add(attempt)\n\n    def load(self):\n        with self.app.app_context():\n            print(\"Creating {:d} journalists...\".format(self.journalist_count))\n            for i in range(1, self.journalist_count + 1):\n                self.new_journalist()\n                if i % min(10, max(1, int(self.journalist_count / 10))) == 0:\n                    sys.stdout.write(\"{}\\r{}\".format(\" \" * len(str(self.journalist_count + 1)), i))\n            print(\"\\n\")\n            db.session.commit()\n\n            print(\"Creating {:d} sources...\".format(self.source_count))\n            for i in range(1, self.source_count + 1):\n                self.new_source()\n                if i % min(10, max(1, int(self.source_count / 10))) == 0:\n                    sys.stdout.write(\"{}\\r{}\".format(\" \" * len(str(self.source_count + 1)), i))\n            print(\"\\n\")\n            db.session.commit()\n\n            print(\n                \"Creating submissions ({:d} each) for each source...\".format(\n                    self.submissions_per_source\n                )\n            )\n            for sid in self.sources:\n                for _ in range(1, self.submissions_per_source + 1):\n                    self.new_submission(sid)\n            db.session.commit()\n\n            print(\"Starring {:.2f}% of all sources...\".format(self.source_star_fraction * 100))\n            for sid in random.sample(\n                self.sources, int(self.source_count * self.source_star_fraction)\n            ):\n                self.new_source_star(sid)\n            db.session.commit()\n\n            print(\n                \"Creating replies ({:d} each) for {:.2f}% of sources...\".format(\n                    self.replies_per_source, self.source_reply_fraction * 100\n                )\n            )\n            for sid in random.sample(\n                self.sources, int(self.source_count * self.source_reply_fraction)\n            ):\n                jid = random.choice(self.journalists)\n                for _ in range(self.replies_per_source):\n                    self.new_reply(jid, sid)\n            db.session.commit()\n\n            for jid in self.journalists:\n                self.new_journalist_login_attempt(jid)\n            db.session.commit()\n\n\ndef arg_parser():\n    parser = ArgumentParser(\n        path.basename(__file__), description=\"Loads data into the database for testing upgrades\"\n    )\n    parser.add_argument(\n        \"--journalist-count\",\n        type=positive_int,\n        default=10,\n        help=(\"Number of journalists to create\"),\n    )\n    parser.add_argument(\n        \"--source-count\", type=positive_int, default=50, help=(\"Number of sources to create\")\n    )\n    parser.add_argument(\n        \"--submissions-per-source\",\n        type=positive_int,\n        default=1,\n        help=(\"Number of submissions to create for each source\"),\n    )\n    parser.add_argument(\n        \"--replies-per-source\",\n        type=positive_int,\n        default=1,\n        help=(\"Number of replies to create for each source\"),\n    )\n    parser.add_argument(\n        \"--source-star-fraction\",\n        type=fraction,\n        default=0.1,\n        help=(\"Fraction of sources to star\"),\n    )\n    parser.add_argument(\n        \"--source-reply-fraction\",\n        type=fraction,\n        default=0.5,\n        help=(\"Fraction of sources to reply to\"),\n    )\n    return parser\n\n\ndef main():\n    args = arg_parser().parse_args()\n    print(\"Loading data. This may take a while.\")\n    QaLoader(\n        sdconfig,\n        args.journalist_count,\n        args.source_count,\n        args.submissions_per_source,\n        args.replies_per_source,\n        args.source_star_fraction,\n        args.source_reply_fraction,\n    ).load()\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except KeyboardInterrupt:\n        print(\"\")  # for prompt on a newline\n        sys.exit(1)\n", "path": "securedrop/qa_loader.py"}], "after_files": [{"content": "#!/opt/venvs/securedrop-app-code/bin/python\n# -*- coding: utf-8 -*-\n\nimport os\nimport random\nimport string\nimport sys\nfrom argparse import ArgumentParser\nfrom datetime import datetime\nfrom itertools import cycle\nfrom os import path\n\nfrom flask import current_app\n\nfrom crypto_util import DICEWARE_SAFE_CHARS\nfrom db import db\nfrom journalist_app import create_app\nfrom models import Journalist, JournalistLoginAttempt, Reply, Source, SourceStar, Submission\nfrom sdconfig import config as sdconfig\n\n\ndef random_bool():\n    return bool(random.getrandbits(1))\n\n\ndef random_chars(len, nullable, chars=string.ascii_letters):\n    if nullable and random_bool():\n        return None\n    else:\n        return \"\".join([random.choice(chars) for _ in range(len)])\n\n\ndef bool_or_none():\n    return random.choice([True, False, None])\n\n\ndef random_datetime(nullable):\n    if nullable and random_bool():\n        return None\n    else:\n        now = datetime.now()\n        return datetime(\n            year=random.randint(2013, now.year),\n            month=random.randint(1, now.month),\n            day=random.randint(1, now.day),\n            hour=random.randint(0, 23),\n            minute=random.randint(0, 59),\n            second=random.randint(0, 59),\n            microsecond=random.randint(0, 1000),\n        )\n\n\ndef positive_int(s):\n    i = int(s)\n    if i < 1:\n        raise ValueError(\"{} is not >= 1\".format(s))\n    return i\n\n\ndef fraction(s):\n    f = float(s)\n    if 0 <= f <= 1:\n        return f\n    raise ValueError(\"{} should be a float between 0 and 1\".format(s))\n\n\nsubmissions = cycle(\n    [\n        \"This is a test submission without markup!\",\n        'This is a test submission with markup and characters such as \\, \\\\, \\', \" and \". '\n        + \"<strong>This text should not be bold</strong>!\",  # noqa: W605, E501\n    ]\n)\n\n\nreplies = cycle(\n    [\n        \"This is a test reply without markup!\",\n        'This is a test reply with markup and characters such as \\, \\\\, \\', \" and \". '\n        + \"<strong>This text should not be bold</strong>!\",  # noqa: W605, E501\n    ]\n)\n\n\nclass QaLoader(object):\n    def __init__(\n        self,\n        config,\n        journalist_count=10,\n        source_count=50,\n        submissions_per_source=1,\n        replies_per_source=1,\n        source_star_fraction=0.1,\n        source_reply_fraction=0.5,\n    ):\n        \"\"\"\n        source_star_fraction and source_reply_fraction are simply the\n        fraction of sources starred or replied to.\n        \"\"\"\n        self.config = config\n        self.app = create_app(config)\n\n        self.journalist_count = journalist_count\n        self.source_count = source_count\n        self.submissions_per_source = submissions_per_source\n        self.replies_per_source = replies_per_source\n        self.source_star_fraction = source_star_fraction\n        self.source_reply_fraction = source_reply_fraction\n\n        self.journalists = []\n        self.sources = []\n\n    def new_journalist(self):\n        # Make a diceware-like password\n        pw = \" \".join(\n            [random_chars(3, nullable=False, chars=DICEWARE_SAFE_CHARS) for _ in range(7)]\n        )\n        journalist = Journalist(\n            username=random_chars(random.randint(3, 32), nullable=False),\n            password=pw,\n            is_admin=random_bool(),\n        )\n        if random_bool():\n            # to add legacy passwords back in\n            journalist.passphrase_hash = None\n            journalist.pw_salt = random_chars(32, nullable=False).encode(\"utf-8\")\n            journalist.pw_hash = random_chars(64, nullable=False).encode(\"utf-8\")\n\n        journalist.is_admin = bool_or_none()\n\n        journalist.is_totp = bool_or_none()\n        journalist.hotp_counter = random.randint(-1000, 1000) if random_bool() else None\n        journalist.created_on = random_datetime(nullable=True)\n        journalist.last_access = random_datetime(nullable=True)\n\n        db.session.add(journalist)\n        db.session.flush()\n        self.journalists.append(journalist.id)\n\n    def new_source(self):\n        codename = current_app.crypto_util.genrandomid()\n        filesystem_id = current_app.crypto_util.hash_codename(codename)\n        journalist_designation = current_app.crypto_util.display_id()\n        source = Source(filesystem_id, journalist_designation)\n        db.session.add(source)\n        db.session.flush()\n\n        # Generate submissions directory and generate source key\n        os.mkdir(current_app.storage.path(source.filesystem_id))\n        current_app.crypto_util.genkeypair(source.filesystem_id, codename)\n\n        self.sources.append(source.id)\n\n    def new_submission(self, source_id):\n        source = Source.query.get(source_id)\n\n        source.interaction_count += 1\n        fpath = current_app.storage.save_message_submission(\n            source.filesystem_id,\n            source.interaction_count,\n            source.journalist_filename,\n            next(submissions),\n        )\n        submission = Submission(source, fpath)\n        db.session.add(submission)\n\n        source.pending = False\n        source.last_updated = datetime.utcnow()\n\n        db.session.flush()\n\n    def new_source_star(self, source_id):\n        source = Source.query.get(source_id)\n        star = SourceStar(source, bool_or_none())\n        db.session.add(star)\n\n    def new_reply(self, journalist_id, source_id):\n        source = Source.query.get(source_id)\n\n        journalist = Journalist.query.get(journalist_id)\n\n        source.interaction_count += 1\n        source.last_updated = datetime.utcnow()\n\n        fname = \"{}-{}-reply.gpg\".format(source.interaction_count, source.journalist_filename)\n        current_app.crypto_util.encrypt(\n            next(replies),\n            [\n                current_app.crypto_util.get_fingerprint(source.filesystem_id),\n                sdconfig.JOURNALIST_KEY\n            ],\n            current_app.storage.path(source.filesystem_id, fname),\n        )\n\n        reply = Reply(journalist, source, fname)\n        db.session.add(reply)\n        db.session.flush()\n\n    def new_journalist_login_attempt(self, journalist_id):\n        journalist = Journalist.query.get(journalist_id)\n        attempt = JournalistLoginAttempt(journalist)\n        attempt.timestamp = random_datetime(nullable=True)\n        db.session.add(attempt)\n\n    def load(self):\n        with self.app.app_context():\n            print(\"Creating {:d} journalists...\".format(self.journalist_count))\n            for i in range(1, self.journalist_count + 1):\n                self.new_journalist()\n                if i % min(10, max(1, int(self.journalist_count / 10))) == 0:\n                    sys.stdout.write(\"{}\\r{}\".format(\" \" * len(str(self.journalist_count + 1)), i))\n            print(\"\\n\")\n            db.session.commit()\n\n            print(\"Creating {:d} sources...\".format(self.source_count))\n            for i in range(1, self.source_count + 1):\n                self.new_source()\n                if i % min(10, max(1, int(self.source_count / 10))) == 0:\n                    sys.stdout.write(\"{}\\r{}\".format(\" \" * len(str(self.source_count + 1)), i))\n            print(\"\\n\")\n            db.session.commit()\n\n            print(\n                \"Creating submissions ({:d} each) for each source...\".format(\n                    self.submissions_per_source\n                )\n            )\n            for sid in self.sources:\n                for _ in range(1, self.submissions_per_source + 1):\n                    self.new_submission(sid)\n            db.session.commit()\n\n            print(\"Starring {:.2f}% of all sources...\".format(self.source_star_fraction * 100))\n            for sid in random.sample(\n                self.sources, int(self.source_count * self.source_star_fraction)\n            ):\n                self.new_source_star(sid)\n            db.session.commit()\n\n            print(\n                \"Creating replies ({:d} each) for {:.2f}% of sources...\".format(\n                    self.replies_per_source, self.source_reply_fraction * 100\n                )\n            )\n            for sid in random.sample(\n                self.sources, int(self.source_count * self.source_reply_fraction)\n            ):\n                jid = random.choice(self.journalists)\n                for _ in range(self.replies_per_source):\n                    self.new_reply(jid, sid)\n            db.session.commit()\n\n            for jid in self.journalists:\n                self.new_journalist_login_attempt(jid)\n            db.session.commit()\n\n\ndef arg_parser():\n    parser = ArgumentParser(\n        path.basename(__file__), description=\"Loads data into the database for testing upgrades\"\n    )\n    parser.add_argument(\n        \"--journalist-count\",\n        type=positive_int,\n        default=10,\n        help=(\"Number of journalists to create\"),\n    )\n    parser.add_argument(\n        \"--source-count\", type=positive_int, default=50, help=(\"Number of sources to create\")\n    )\n    parser.add_argument(\n        \"--submissions-per-source\",\n        type=positive_int,\n        default=1,\n        help=(\"Number of submissions to create for each source\"),\n    )\n    parser.add_argument(\n        \"--replies-per-source\",\n        type=positive_int,\n        default=1,\n        help=(\"Number of replies to create for each source\"),\n    )\n    parser.add_argument(\n        \"--source-star-fraction\",\n        type=fraction,\n        default=0.1,\n        help=(\"Fraction of sources to star\"),\n    )\n    parser.add_argument(\n        \"--source-reply-fraction\",\n        type=fraction,\n        default=0.5,\n        help=(\"Fraction of sources to reply to\"),\n    )\n    return parser\n\n\ndef main():\n    args = arg_parser().parse_args()\n    print(\"Loading data. This may take a while.\")\n    QaLoader(\n        sdconfig,\n        args.journalist_count,\n        args.source_count,\n        args.submissions_per_source,\n        args.replies_per_source,\n        args.source_star_fraction,\n        args.source_reply_fraction,\n    ).load()\n\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except KeyboardInterrupt:\n        print(\"\")  # for prompt on a newline\n        sys.exit(1)\n", "path": "securedrop/qa_loader.py"}]}