18 |
24 |
25 | {% for category,items in results %}
26 | {% if items %}
27 |
{{ category.verbose_name|title }}
28 |
29 | {% for r in items %}
30 | - {{ r }}: {{ r.short_description|safe }}
31 | {% endfor %}
32 |
33 | {% endif %}
34 | {% empty %}
35 | No results found
36 | {% endfor %}
37 |
38 | {% endblock %}
39 |
--------------------------------------------------------------------------------
/search/search_site.py:
--------------------------------------------------------------------------------
1 | from django_search_views.search import SearchCategory, Search
2 |
3 | from srd20.models import Spell, Feat, Monster
4 |
5 | # These are the fields searched by the full text search
6 | SEARCH_FIELDS = (
7 | 'name',
8 | 'altname',
9 | 'short_description',
10 | 'description',
11 | 'school',
12 | 'subschool',
13 | 'descriptor',
14 | 'material_components',
15 | 'reference',
16 | )
17 |
18 |
19 | class SpellByName(SearchCategory):
20 | model = Spell
21 | lookups = ['name__icontains']
22 | def verbose_name(self): return u'spells'
23 |
24 | class SpellFullText(SearchCategory):
25 | model = Spell
26 | lookups = [field+'__icontains' for field in SEARCH_FIELDS]
27 | def verbose_name(self): return u'spells (full text)'
28 |
29 | class FeatByName(SearchCategory):
30 | model = Feat
31 | lookups = ['name__icontains']
32 | def verbose_name(self): return u'feats'
33 |
34 | class MonsterByName(SearchCategory):
35 | model = Monster
36 | lookups = ['name__icontains']
37 | def verbose_name(self): return u'monsters'
38 |
39 | class SiteSearch(Search):
40 | categories = [SpellByName, SpellFullText, FeatByName, MonsterByName]
41 |
42 | site = SiteSearch()
43 | urls = site.urls()
44 |
45 |
--------------------------------------------------------------------------------
/encounter/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | class Encounter(models.Model):
4 | name = models.CharField(max_length=255)
5 | current_round = models.PositiveIntegerField(default=0)
6 | current_initiative = models.IntegerField(default=999)
7 | notes = models.TextField(blank=True)
8 | # participants = reverse from Participant.encounter
9 | #treasure = models.TextField(blank=True)
10 |
11 | def __unicode__(self):
12 | return self.name
13 |
14 | def current_actors(self):
15 | """The list of actors in the current initiative"""
16 | return self.participants.filter(initiative=self.current_initiative)
17 |
18 | class Participant(models.Model):
19 | encounter = models.ForeignKey(Encounter, related_name='participants')
20 | label = models.CharField(max_length=32)
21 | current_hp = models.IntegerField(blank=True, null=True)
22 | initiative = models.IntegerField(blank=True, null=True)
23 | notes = models.TextField(blank=True)
24 | #stats = ...
25 | #conditions = [(Condition, final_round, final_initiative)]
26 |
27 | #class ParticipantCondition
28 | # who = Participant
29 | # what = Condition
30 | # until_round = int
31 | # until_initiative = int
32 | # notes = ...
33 |
34 |
35 | #class Condition:
36 | # affects_defense = True
37 | # affects_offense = True
38 | # can_act = True
39 |
40 |
--------------------------------------------------------------------------------
/browse/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import get_object_or_404, render_to_response
2 | from django.template import RequestContext
3 | from django.views.generic import TemplateView
4 |
5 | from srd20.models import Spell, Feat, Monster
6 |
7 | def spell_detail(request, slug):
8 | spell = get_object_or_404(Spell, altname=slug)
9 | return render_to_response('browse/spell.html',
10 | {
11 | 'spell': spell,
12 | 'editable': request.user.has_perm('srd20.change_spell'),
13 | },
14 | context_instance=RequestContext(request)
15 | )
16 |
17 |
18 | def feat_detail(request, slug):
19 | feat = get_object_or_404(Feat, altname=slug)
20 | return render_to_response('browse/feat.html',
21 | {
22 | 'feat': feat,
23 | 'editable': request.user.has_perm('srd20.change_feat'),
24 | },
25 | context_instance=RequestContext(request)
26 | )
27 |
28 | def monster_detail(request, slug):
29 | monster = get_object_or_404(Monster, altname=slug)
30 | return render_to_response('browse/monster.html',
31 | {
32 | 'monster': monster,
33 | 'editable': request.user.has_perm('srd20.change_monster'),
34 | },
35 | context_instance=RequestContext(request)
36 | )
37 |
38 | class Favorites(TemplateView):
39 | template_name = "browse/favorites.html"
40 |
41 | favorites = Favorites.as_view()
42 |
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 |
3 | {% block title %}SRD/d20 browser{% endblock title%}
4 |
5 | {% block extrastyle %}
6 |
20 |
21 |
Feats
22 |
23 | {% likes user "srd20.Feat" as like_list %}
24 |
25 |
26 | {% for l in like_list %}
27 | - {{ l.receiver }}: {{ l.receiver.short_description }}
28 | {% empty %}
29 | - No favorite feats
30 | {% endfor %}
31 |
32 |
33 |
Spells
34 |
35 | {% likes user "srd20.Spell" as like_list %}
36 |
37 |
38 | {% for l in like_list %}
39 | - {{ l.receiver }}: {{ l.receiver.short_description }}
40 | {% empty %}
41 | - No favorite spells
42 | {% endfor %}
43 |
44 |
45 | {% likes user "srd20.Monster" as like_list %}
46 |
47 |
Monsters
48 |
49 |
50 | {% for l in like_list %}
51 | - {{ l.receiver }}: {{ l.receiver.short_description }}
52 | {% empty %}
53 | - No favorite monsters
54 | {% endfor %}
55 |
56 |
57 |
58 | {% endblock %}
59 |
60 |
61 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | This software is a browser for D20 system data.
2 |
3 | This is capable of using Open Game Content licensed SRD data, and custom
4 | personal data.
5 |
6 | Requirements
7 | ============
8 |
9 | * Python 2.6 or a higher version of Python 2.x
10 | * Django 1.2
11 | * Django South 0.7.x (tested with 0.7.3)
12 | * Some database engine supported by Django (SQLite3 used in the default config)
13 |
14 | Quick start instructions
15 | ========================
16 |
17 | The following instructions are for installing and deploying locally. If you
18 | need to install this in a public server read the Django documentation about
19 | deploying Django sites.
20 |
21 | Install the requirements, and virtualenv. virtualenv is not a requirement, but
22 | it is a good idea to have. Then run the following commands::
23 |
24 | virtualenv --no-site-packages srd20
25 | source srd20/bin/activate
26 | cd srd20
27 | easy_install django south
28 | git clone https://github.com/machinalis/django-srd20.git django_srd20
29 | cd django_srd20
30 |
31 | At this point you can customize settings.py or add a local_settings.py; but
32 | the defaults should work.
33 |
34 | The first time you should should setup the database in the following way::
35 |
36 | ./manage.py syncdb # This will ask for an admin password
37 | ./manage migrate
38 |
39 | Doing this again later won't break anything, but is not required. You can also
40 | load some of the provided OGL licensed content into the database with the
41 | following command::
42 |
43 | ./manage.py loaddata srd20/fixtures/srd.json
44 |
45 | To run a development server that you can access with a web browser::
46 |
47 | ./manage runserver
48 |
49 | While the above is running (until interrupting with CTRL+C) you can point a
50 | browser to http://127.0.0.1:8000/ and use the application
51 |
52 |
--------------------------------------------------------------------------------
/pathfinder/spell-descriptions.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 |
4 | # Importer for spell description. This is separate from the spell
5 | # importer because descriptions are only on the spell lists
6 | # This outputs a csv with two columns: slug and description
7 |
8 | import sys, os, csv
9 | from pyquery import PyQuery as Q
10 | import lxml.etree
11 |
12 | def pqiter(pq):
13 | """
14 | Iterator on a PyQuery object that produces PyQuery objects, instead
15 | of plain XML elements
16 | """
17 | index = 0
18 | while index < len(pq):
19 | yield pq.eq(index)
20 | index += 1
21 |
22 | # Iterate over input files
23 | output = csv.writer(sys.stdout)
24 | for filename in sys.argv[1:]:
25 | list_html = Q(filename=filename, parser='html')
26 | spells = list_html("#body p b a")
27 |
28 | for s in pqiter(spells):
29 | target = s.attr('href')
30 | try:
31 | target = target.split('#')[1]
32 | except IndexError:
33 | target = os.path.basename(target)
34 | sys.stderr.write("Looking up rename for %s\n" % target)
35 | target = {
36 | 'deathwatch.html': '_deathwatch',
37 | 'fly.html': '_fly',
38 | 'fogCloud.html': '_fog-cloud',
39 | }[target]
40 | if target.startswith('_'):
41 | target = target[1:]
42 | target = target.replace(',', '') # Remove commas (usde in ", mass" ", greater", etc)
43 |
44 | desc_elem = s[0].getparent()
45 | description = desc_elem.tail or ''
46 | while desc_elem.getnext() is not None:
47 | desc_elem = desc_elem.getnext()
48 | description += lxml.etree.tostring(desc_elem)
49 |
50 | description = description.split(':')[1].strip()
51 | output.writerow([target.encode('utf-8'), description.encode('utf-8')])
52 |
--------------------------------------------------------------------------------
/browse/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from django.contrib.auth.models import User, Permission
3 |
4 | class BrowseTest(TestCase):
5 | fixtures = ['srd.json']
6 |
7 | def test_get(self):
8 | response = self.client.get('/browse/spell/alarm/')
9 | # Check that we got a result
10 | self.assertEqual(200, response.status_code)
11 | # Check that the requested spellwas in the context
12 | self.assertEqual('alarm', response.context['spell'].altname)
13 |
14 | def test_get_complex_slug(self):
15 | """This test intended to check that the slug regex is OK and works with dashes and uppercase"""
16 | response = self.client.get('/browse/spell/summon-natures-ally-VI/')
17 | self.assertEqual(200, response.status_code)
18 | self.assertEqual('summon-natures-ally-VI', response.context['spell'].altname)
19 |
20 | def test_get_404(self):
21 | response = self.client.get('/browse/spell/does-not-exist/')
22 | # Check that we got a 404 result
23 | self.assertEqual(404, response.status_code)
24 |
25 | def test_anonymous_cant_edit(self):
26 | response = self.client.get('/browse/spell/alarm/')
27 | self.assertEqual(200, response.status_code)
28 | self.assertEqual(False, response.context['editable'])
29 |
30 | def test_authenticated_cantedit(self):
31 | authenticated, _ = User.objects.get_or_create(
32 | username='testuser',
33 | password='*',
34 | is_staff=True
35 | )
36 | authenticated.set_password('test')
37 | authenticated.user_permissions.add(Permission.objects.get(codename='change_spell', content_type__app_label='srd20'))
38 | authenticated.save()
39 | self.client.login(username='testuser',password='test')
40 | response = self.client.get('/browse/spell/alarm/')
41 | self.assertEqual(200, response.status_code)
42 | self.assertEqual(True, response.context['editable'])
43 |
44 |
--------------------------------------------------------------------------------
/srd20/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from srd20.models import Spell, Feat, CharacterClass, Monster, MonsterAbility
3 |
4 | class SpellAdmin(admin.ModelAdmin):
5 | list_display = ('name', 'level', 'short_description')
6 | list_filter = ('school',)
7 | search_fields = ('name',)
8 | prepopulated_fields = {'altname': ('name',)}
9 |
10 | fieldsets = (
11 | (None, {
12 | 'fields': ('name', 'altname', ('school', 'subschool'), 'descriptor', 'level', 'reference')
13 | }),
14 | ('Properties', {
15 | 'fields': ('components', 'range', ('target', 'area', 'effect'), 'duration', 'saving_throw', 'spell_resistance')
16 | }),
17 | ('Epic requirements', {
18 | 'fields': ('spellcraft_dc', 'to_develop'),
19 | 'classes': ("collapse",)
20 | }),
21 | ('Description', {
22 | 'fields': ('short_description', 'description', 'verbal_components',
23 | 'material_components', 'arcane_material_components', 'focus',
24 | 'arcane_focus', 'cleric_focus', 'druid_focus','xp_cost')
25 | }),
26 | )
27 |
28 | class FeatAdmin(admin.ModelAdmin):
29 | list_display = ('name', 'type')
30 | list_filter = ('type',)
31 | search_fields = ('name',)
32 | prepopulated_fields = {'altname': ('name',)}
33 |
34 | fieldsets = (
35 | (None, {
36 | 'fields': ('name', 'altname', 'type', ('multiple', 'stack'), 'prerequisite', 'choice')
37 | }),
38 | ('Description', {
39 | 'fields': ('benefit', 'normal', 'special')
40 | }),
41 | ('Source', {
42 | 'fields': ('reference',),
43 | }),
44 | )
45 |
46 | class MonsterAbilityInline(admin.TabularInline):
47 | model = MonsterAbility
48 |
49 | class MonsterAdmin(admin.ModelAdmin):
50 | list_display = ('name', 'alignment', 'size', 'type', 'environment', 'cr')
51 | list_filter = ('cr', 'type', 'size', 'alignment', 'reference')
52 | search_fields = ('name',)
53 | inlines = [MonsterAbilityInline]
54 |
55 | admin.site.register(Spell, SpellAdmin)
56 | admin.site.register(Feat, FeatAdmin)
57 | admin.site.register(Monster, MonsterAdmin)
58 | admin.site.register(CharacterClass)
59 |
60 |
--------------------------------------------------------------------------------
/browse/templates/browse/feat.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% load phileo_tags %}
4 |
5 | {% block extrastyle %}
6 | {{ block.super }}
7 | {% phileo_css %}
8 |
31 |
32 | {% phileo_widget_js user feat %}
33 |
34 | {% if editable %}
35 |
36 | [edit]
37 |
38 | {% endif %}
39 |
40 |
{{ feat.type }}
41 |
42 | {{ feat.description|safe }}
43 |
44 |
Benefit:{{ feat.benefit|safe }}
45 |
46 | {% if feat.normal %}
47 |
Normal: {{ feat.normal|safe }}
48 | {% endif %}
49 | {% if feat.special %}
50 |
Special: {{ feat.special|safe }}
51 | {% endif %}
52 |
53 |
54 | {% endblock %}
55 |
56 | {% block sidebar %}
57 |
31 |
32 | {% phileo_widget_js user spell %}
33 |
34 | {% if editable %}
35 |
36 | [edit]
37 |
38 | {% endif %}
39 |
40 |
{{ spell.school }}
41 | {% if spell.subschool %}({{ spell.subschool }}){% endif %}
42 | {% if spell.descriptor %}[{{spell.descriptor}}]{% endif %}
43 |
44 |
45 |
46 | {% if spell.spellcraft_dc %}
47 |
48 | | Spellcraft DC: |
49 | {{ spell.spellcraft_dc }} |
50 |
51 | {% endif %}
52 | {% if spell.to_develop %}
53 |
54 | | To Develop: |
55 | {{ spell.to_develop }} |
56 |
57 | {% endif %}
58 |
59 |
60 | {{ spell.description|safe }}
61 |
62 | {% if spell.verbal_components %}
63 |
Verbal Components: {{ spell.verbal_components }}
64 | {% endif %}
65 | {% if spell.material_components %}
66 |
Material Component: {{ spell.material_components }}
67 | {% endif %}
68 | {% if spell.arcane_material_components %}
69 |
Arcane Material Component: {{ spell.arcane_material_components }}
70 | {% endif %}
71 | {% if spell.focus %}
72 |
Focus: {{ spell.focus }}
73 | {% endif %}
74 | {% if spell.arcane_focus %}
75 |
Arcane Focus: {{ spell.arcane_focus }}
76 | {% endif %}
77 | {% if spell.cleric_focus %}
78 |
Cleric Focus: {{ spell.cleric_focus }}
79 | {% endif %}
80 | {% if spell.druid_focus %}
81 |
Druid Focus: {{ spell.druid_focus }}
82 | {% endif %}
83 | {% if spell.xp_cost %}
84 |
XP Cost: {{ spell.xp_cost }}
85 | {% endif %}
86 |
87 | {% endblock %}
88 |
89 | {% block sidebar %}
90 |