populate.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.utils.populate
  4. ~~~~~~~~~~~~~~~~~~~~~~
  5. A module that makes creating data more easily
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. from __future__ import unicode_literals
  10. import collections
  11. import logging
  12. import os
  13. from flask import current_app
  14. from sqlalchemy_utils.functions import create_database, database_exists
  15. from alembic.util.exc import CommandError
  16. from flaskbb.extensions import alembic, db
  17. from flaskbb.forum.models import Category, Forum, Post, Topic
  18. from flaskbb.management.models import Setting, SettingsGroup
  19. from flaskbb.user.models import Group, User
  20. logger = logging.getLogger(__name__)
  21. def delete_settings_from_fixture(fixture):
  22. """Deletes the settings from a fixture from the database.
  23. Returns the deleted groups and settings.
  24. :param fixture: The fixture that should be deleted.
  25. """
  26. deleted_settings = {}
  27. for settingsgroup in fixture:
  28. group = SettingsGroup.query.filter_by(key=settingsgroup[0]).first()
  29. deleted_settings[group] = []
  30. for settings in settingsgroup[1]["settings"]:
  31. setting = Setting.query.filter_by(key=settings[0]).first()
  32. if setting:
  33. deleted_settings[group].append(setting)
  34. setting.delete()
  35. group.delete()
  36. return deleted_settings
  37. def create_settings_from_fixture(fixture):
  38. """Inserts the settings from a fixture into the database.
  39. Returns the created groups and settings.
  40. :param fixture: The fixture which should inserted.
  41. """
  42. created_settings = {}
  43. for settingsgroup in fixture:
  44. group = SettingsGroup(
  45. key=settingsgroup[0],
  46. name=settingsgroup[1]["name"],
  47. description=settingsgroup[1]["description"]
  48. )
  49. group.save()
  50. created_settings[group] = []
  51. for settings in settingsgroup[1]["settings"]:
  52. setting = Setting(
  53. key=settings[0],
  54. value=settings[1]["value"],
  55. value_type=settings[1]["value_type"],
  56. name=settings[1]["name"],
  57. description=settings[1]["description"],
  58. extra=settings[1].get("extra", ""), # Optional field
  59. settingsgroup=group.key
  60. )
  61. if setting:
  62. setting.save()
  63. created_settings[group].append(setting)
  64. return created_settings
  65. def update_settings_from_fixture(fixture, overwrite_group=False,
  66. overwrite_setting=False):
  67. """Updates the database settings from a fixture.
  68. Returns the updated groups and settings.
  69. :param fixture: The fixture which should be inserted/updated.
  70. :param overwrite_group: Set this to ``True`` if you want to overwrite
  71. the group if it already exists.
  72. Defaults to ``False``.
  73. :param overwrite_setting: Set this to ``True`` if you want to overwrite the
  74. setting if it already exists.
  75. Defaults to ``False``.
  76. """
  77. updated_settings = collections.defaultdict(list)
  78. for settingsgroup in fixture:
  79. group = SettingsGroup.query.filter_by(key=settingsgroup[0]).first()
  80. if (group is not None and overwrite_group) or group is None:
  81. if group is not None:
  82. group.name = settingsgroup[1]["name"]
  83. group.description = settingsgroup[1]["description"]
  84. else:
  85. group = SettingsGroup(
  86. key=settingsgroup[0],
  87. name=settingsgroup[1]["name"],
  88. description=settingsgroup[1]["description"]
  89. )
  90. group.save()
  91. for settings in settingsgroup[1]["settings"]:
  92. setting = Setting.query.filter_by(key=settings[0]).first()
  93. if setting is not None:
  94. setting_is_different = (
  95. setting.value != settings[1]["value"]
  96. or setting.value_type != settings[1]["value_type"]
  97. or setting.name != settings[1]["name"]
  98. or setting.description != settings[1]["description"]
  99. or setting.extra != settings[1].get("extra", "")
  100. or setting.settingsgroup != group.key
  101. )
  102. if (
  103. setting is not None and
  104. overwrite_setting and
  105. setting_is_different
  106. ) or setting is None:
  107. if setting is not None:
  108. setting.value = settings[1]["value"]
  109. setting.value_type = settings[1]["value_type"]
  110. setting.name = settings[1]["name"]
  111. setting.description = settings[1]["description"]
  112. setting.extra = settings[1].get("extra", "")
  113. setting.settingsgroup = group.key
  114. else:
  115. setting = Setting(
  116. key=settings[0],
  117. value=settings[1]["value"],
  118. value_type=settings[1]["value_type"],
  119. name=settings[1]["name"],
  120. description=settings[1]["description"],
  121. extra=settings[1].get("extra", ""),
  122. settingsgroup=group.key
  123. )
  124. setting.save()
  125. updated_settings[group].append(setting)
  126. return updated_settings
  127. def create_default_settings():
  128. """Creates the default settings."""
  129. from flaskbb.fixtures.settings import fixture
  130. create_settings_from_fixture(fixture)
  131. def create_default_groups():
  132. """This will create the 5 default groups."""
  133. from flaskbb.fixtures.groups import fixture
  134. result = []
  135. for key, value in fixture.items():
  136. group = Group(name=key)
  137. for k, v in value.items():
  138. setattr(group, k, v)
  139. group.save()
  140. result.append(group)
  141. return result
  142. def create_user(username, password, email, groupname):
  143. """Creates a user.
  144. Returns the created user.
  145. :param username: The username of the user.
  146. :param password: The password of the user.
  147. :param email: The email address of the user.
  148. :param groupname: The name of the group to which the user
  149. should belong to.
  150. """
  151. if groupname == "member":
  152. group = Group.get_member_group()
  153. else:
  154. group = Group.query.filter(getattr(Group, groupname) == True).first()
  155. user = User.create(username=username, password=password, email=email,
  156. primary_group_id=group.id, activated=True)
  157. return user
  158. def update_user(username, password, email, groupname):
  159. """Update an existing user.
  160. Returns the updated user.
  161. :param username: The username of the user.
  162. :param password: The password of the user.
  163. :param email: The email address of the user.
  164. :param groupname: The name of the group to which the user
  165. should belong to.
  166. """
  167. user = User.query.filter_by(username=username).first()
  168. if user is None:
  169. return None
  170. if groupname == "member":
  171. group = Group.get_member_group()
  172. else:
  173. group = Group.query.filter(getattr(Group, groupname) == True).first()
  174. user.password = password
  175. user.email = email
  176. user.primary_group = group
  177. return user.save()
  178. def create_welcome_forum():
  179. """This will create the `welcome forum` with a welcome topic.
  180. Returns True if it's created successfully.
  181. """
  182. if User.query.count() < 1:
  183. return False
  184. user = User.query.filter_by(id=1).first()
  185. category = Category(title="My Category", position=1)
  186. category.save()
  187. forum = Forum(title="Welcome", description="Your first forum",
  188. category_id=category.id)
  189. forum.save()
  190. topic = Topic(title="Welcome!")
  191. post = Post(content="Have fun with your new FlaskBB Forum!")
  192. topic.save(user=user, forum=forum, post=post)
  193. return True
  194. def create_test_data(users=5, categories=2, forums=2, topics=1, posts=1):
  195. """Creates 5 users, 2 categories and 2 forums in each category.
  196. It also creates a new topic topic in each forum with a post.
  197. Returns the amount of created users, categories, forums, topics and posts
  198. as a dict.
  199. :param users: The number of users.
  200. :param categories: The number of categories.
  201. :param forums: The number of forums which are created in each category.
  202. :param topics: The number of topics which are created in each forum.
  203. :param posts: The number of posts which are created in each topic.
  204. """
  205. create_default_groups()
  206. create_default_settings()
  207. data_created = {'users': 0, 'categories': 0, 'forums': 0,
  208. 'topics': 0, 'posts': 0}
  209. # create 5 users
  210. for u in range(1, users + 1):
  211. username = "test%s" % u
  212. email = "test%s@example.org" % u
  213. user = User(username=username, password="test", email=email)
  214. user.primary_group_id = u
  215. user.activated = True
  216. user.save()
  217. data_created['users'] += 1
  218. user1 = User.query.filter_by(id=1).first()
  219. user2 = User.query.filter_by(id=2).first()
  220. # lets send them a few private messages
  221. for i in range(1, 3):
  222. # TODO
  223. pass
  224. # create 2 categories
  225. for i in range(1, categories + 1):
  226. category_title = "Test Category %s" % i
  227. category = Category(title=category_title,
  228. description="Test Description")
  229. category.save()
  230. data_created['categories'] += 1
  231. # create 2 forums in each category
  232. for j in range(1, forums + 1):
  233. if i == 2:
  234. j += 2
  235. forum_title = "Test Forum %s %s" % (j, i)
  236. forum = Forum(title=forum_title, description="Test Description",
  237. category_id=i)
  238. forum.save()
  239. data_created['forums'] += 1
  240. for t in range(1, topics + 1):
  241. # create a topic
  242. topic = Topic(title="Test Title %s" % j)
  243. post = Post(content="Test Content")
  244. topic.save(post=post, user=user1, forum=forum)
  245. data_created['topics'] += 1
  246. for p in range(1, posts + 1):
  247. # create a second post in the forum
  248. post = Post(content="Test Post")
  249. post.save(user=user2, topic=topic)
  250. data_created['posts'] += 1
  251. return data_created
  252. def insert_bulk_data(topic_count=10, post_count=100):
  253. """Creates a specified number of topics in the first forum with
  254. each topic containing a specified amount of posts.
  255. Returns the number of created topics and posts.
  256. :param topics: The amount of topics in the forum.
  257. :param posts: The number of posts in each topic.
  258. """
  259. user1 = User.query.filter_by(id=1).first()
  260. user2 = User.query.filter_by(id=2).first()
  261. forum = Forum.query.filter_by(id=1).first()
  262. last_post = Post.query.order_by(Post.id.desc()).first()
  263. last_post_id = 1 if last_post is None else last_post.id
  264. created_posts = 0
  265. created_topics = 0
  266. posts = []
  267. if not (user1 or user2 or forum):
  268. return False
  269. db.session.begin(subtransactions=True)
  270. for i in range(1, topic_count + 1):
  271. last_post_id += 1
  272. # create a topic
  273. topic = Topic(title="Test Title %s" % i)
  274. post = Post(content="First Post")
  275. topic.save(post=post, user=user1, forum=forum)
  276. created_topics += 1
  277. # create some posts in the topic
  278. for j in range(1, post_count + 1):
  279. last_post_id += 1
  280. post = Post(content="Some other Post", user=user2, topic=topic.id)
  281. topic.last_updated = post.date_created
  282. topic.post_count += 1
  283. # FIXME: Is there a way to ignore IntegrityErrors?
  284. # At the moment, the first_post_id is also the last_post_id.
  285. # This does no harm, except that in the forums view, you see
  286. # the information for the first post instead of the last one.
  287. # I run a little benchmark:
  288. # 5.3643078804 seconds to create 100 topics and 10000 posts
  289. # Using another method (where data integrity is ok) I benchmarked
  290. # these stats:
  291. # 49.7832770348 seconds to create 100 topics and 10000 posts
  292. # Uncomment the line underneath and the other line to reduce
  293. # performance but fixes the above mentioned problem.
  294. # topic.last_post_id = last_post_id
  295. created_posts += 1
  296. posts.append(post)
  297. # uncomment this and delete the one below, also uncomment the
  298. # topic.last_post_id line above. This will greatly reduce the
  299. # performance.
  300. # db.session.bulk_save_objects(posts)
  301. db.session.bulk_save_objects(posts)
  302. # and finally, lets update some stats
  303. forum.recalculate(last_post=True)
  304. user1.recalculate()
  305. user2.recalculate()
  306. return created_topics, created_posts
  307. def create_latest_db(target="default@head"):
  308. """Creates the database including the schema using SQLAlchemy's
  309. db.create_all method instead of going through all the database revisions.
  310. The revision will be set to 'head' which indicates the latest alembic
  311. revision.
  312. :param target: The target branch. Defaults to 'default@head'.
  313. """
  314. if not database_exists(db.engine.url):
  315. create_database(db.engine.url)
  316. db.create_all()
  317. alembic.stamp(target=target)
  318. def run_plugin_migrations(plugins=None):
  319. """Runs the migrations for a list of plugins.
  320. :param plugins: A iterable of plugins to run the migrations for. If set
  321. to ``None``, all external plugin migrations will be run.
  322. """
  323. if plugins is None:
  324. plugins = current_app.pluggy.get_external_plugins()
  325. for plugin in plugins:
  326. plugin_name = current_app.pluggy.get_name(plugin)
  327. if not os.path.exists(os.path.join(plugin.__path__[0], "migrations")):
  328. logger.debug("No migrations found for plugin %s" % plugin_name)
  329. continue
  330. try:
  331. alembic.upgrade(target="{}@head".format(plugin_name))
  332. except CommandError as exc:
  333. logger.debug("Couldn't run migrations for plugin {} because of "
  334. "following exception: ".format(plugin_name),
  335. exc_info=exc)