| 181 | class RandomNode(Node): |
| 182 | def __init__(self, nodelist_options, weights=None): |
| 183 | self.nodelist_options = nodelist_options |
| 184 | self.weights = weights |
| 185 | if weights: |
| 186 | self.weight_total = 0 |
| 187 | self.weight_items = {} |
| 188 | for i, option in enumerate(nodelist_options): |
| 189 | self.weight_total += weights[i] |
| 190 | self.weight_items[self.weight_total] = option |
| 191 | self.weight_boundaries = self.weight_items.keys() |
| 192 | self.weight_boundaries.sort() |
| 193 | |
| 194 | def render(self, context): |
| 195 | if self.weights: |
| 196 | from bisect import bisect_left |
| 197 | from random import randint |
| 198 | return self.weight_items[ |
| 199 | self.weight_boundaries[ |
| 200 | bisect_left( |
| 201 | self.weight_boundaries, |
| 202 | randint(0, self.weight_total) |
| 203 | ) |
| 204 | ] |
| 205 | ].render(context) |
| 206 | else: |
| 207 | from random import choice |
| 208 | return choice(self.nodelist_options).render(context) |
| 209 | |
| 620 | def do_random(parser, token): |
| 621 | """ |
| 622 | Output the contents of a random block, with optional weighting. |
| 623 | |
| 624 | The `random` block tag must contain one or more `or` tags, which separate |
| 625 | possible choices; a choice in this context is everything between a |
| 626 | `random` and `or` tag, between two `or` tags, or between an `or` and an |
| 627 | `endrandom` tag. |
| 628 | |
| 629 | Sample usage:: |
| 630 | |
| 631 | {% random %} |
| 632 | You will see me half the time. |
| 633 | {% or %} |
| 634 | You will see me the other half. |
| 635 | {% endrandom %} |
| 636 | |
| 637 | To specify optional weights for the items, include comma-separated integers |
| 638 | in the `random` tag, one for each item in order, like so:: |
| 639 | |
| 640 | {% random 4,1 %} |
| 641 | You will see me four times more often than the other option. |
| 642 | {% or %} |
| 643 | You will see me only one-fifth of the time. |
| 644 | {% endrandom %} |
| 645 | |
| 646 | The chance of a weighted item being shown is its weight value divided by |
| 647 | the total of all weight values. |
| 648 | """ |
| 649 | args = token.contents.split() |
| 650 | if len(args) == 1: |
| 651 | # {% random %} |
| 652 | weights = None |
| 653 | elif len(args) == 2 and ',' in args[1]: |
| 654 | # {% random 4,2,1 %} |
| 655 | try: |
| 656 | weights = [int(w) for w in args[1].split(',') if w] |
| 657 | except ValueError: |
| 658 | raise TemplateSyntaxError("Weight arguments for 'random' must be integers") |
| 659 | else: |
| 660 | raise TemplateSyntaxError("Invalid arguments to 'random': %s" % args) |
| 661 | options = NodeList() |
| 662 | while True: |
| 663 | option = parser.parse(('or', 'endrandom')) |
| 664 | token = parser.next_token() |
| 665 | options.append(option) |
| 666 | if token.contents == 'or': |
| 667 | continue |
| 668 | parser.delete_first_token() |
| 669 | break |
| 670 | if len(options) < 2: |
| 671 | raise TemplateSyntaxError, "'random' tag must have at least two possibilities" |
| 672 | elif weights and len(options) != len(weights): |
| 673 | raise TemplateSyntaxError, "The number of specified weights (%s)" \ |
| 674 | " must equal the number of possible options (%s)" % (len(options), len(weights)) |
| 675 | return RandomNode(options, weights=weights) |
| 676 | |