evewiki 0.0.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
evewiki/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Wiki plugin app for Alliance Auth."""
2
+
3
+ # pylint: disable = invalid-name
4
+ default_app_config = "evewiki.apps.EveWikiConfig"
5
+
6
+ __version__ = "0.0.1.dev1"
evewiki/admin.py ADDED
@@ -0,0 +1,16 @@
1
+ """Admin site."""
2
+
3
+ from django.contrib import admin
4
+
5
+ from .models.logs import Log
6
+ from .models.page_versions import PageVersion
7
+ from .models.pages import Page
8
+ from .models.settings import Setting
9
+
10
+ # from django.contrib import admin
11
+
12
+ # Register your models for the admin site here.
13
+ admin.site.register(Page)
14
+ admin.site.register(Setting)
15
+ admin.site.register(PageVersion)
16
+ admin.site.register(Log)
@@ -0,0 +1,5 @@
1
+ """App settings."""
2
+
3
+ from django.conf import settings
4
+
5
+ EXAMPLE_SETTING_ONE = getattr(settings, "EXAMPLE_SETTING_ONE", None)
evewiki/apps.py ADDED
@@ -0,0 +1,9 @@
1
+ from django.apps import AppConfig
2
+
3
+ from . import __version__
4
+
5
+
6
+ class EveWikiConfig(AppConfig):
7
+ name = "evewiki"
8
+ label = "evewiki"
9
+ verbose_name = f"evewiki v{__version__}"
evewiki/auth_hooks.py ADDED
@@ -0,0 +1,35 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+
3
+ from allianceauth import hooks
4
+ from allianceauth.services.hooks import MenuItemHook, UrlHook
5
+
6
+ from . import urls
7
+
8
+
9
+ class EveWikiMenuItem(MenuItemHook):
10
+ """This class ensures only authorized users will see the menu entry"""
11
+
12
+ def __init__(self):
13
+ # setup menu entry for sidebar
14
+ MenuItemHook.__init__(
15
+ self,
16
+ _("wiki"),
17
+ "fas fa-tree fa-fw",
18
+ "evewiki:index",
19
+ navactive=["evewiki:"],
20
+ )
21
+
22
+ def render(self, request):
23
+ if request.user.has_perm("evewiki.basic_access"):
24
+ return MenuItemHook.render(self, request)
25
+ return ""
26
+
27
+
28
+ @hooks.register("menu_item_hook")
29
+ def register_menu():
30
+ return EveWikiMenuItem()
31
+
32
+
33
+ @hooks.register("url_hook")
34
+ def register_urls():
35
+ return UrlHook(urls, "evewiki", r"^evewiki/")
evewiki/forms.py ADDED
@@ -0,0 +1,29 @@
1
+ from django import forms
2
+
3
+ from .models.pages import Page
4
+
5
+
6
+ class PageForm(forms.ModelForm):
7
+
8
+ # Customised ddl inferring additional context via text-indentation
9
+ parent = forms.ChoiceField(
10
+ choices=Page.list(),
11
+ required=False,
12
+ label="Path",
13
+ help_text=Page._meta.get_field("parent").help_text,
14
+ )
15
+
16
+ class Meta:
17
+ model = Page
18
+ fields = ["title", "parent", "slug", "priority", "states", "groups"]
19
+
20
+ def clean(self):
21
+ """
22
+ Django needs a little help to turn the custom `parent` field back into a model.
23
+ """
24
+ cleaned_data = super().clean()
25
+ parent_id = cleaned_data["parent"]
26
+ cleaned_data["parent"] = (
27
+ Page.objects.get(pk=int(parent_id)) if parent_id else None
28
+ )
29
+ return cleaned_data
@@ -0,0 +1,190 @@
1
+ # Generated by Django 4.2.21 on 2025-05-27 00:35
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ("auth", "0012_alter_user_first_name_max_length"),
13
+ ("authentication", "0024_alter_userprofile_language"),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="General",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.AutoField(
23
+ auto_created=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ verbose_name="ID",
27
+ ),
28
+ ),
29
+ ],
30
+ options={
31
+ "permissions": (
32
+ ("basic_access", "Can access this app"),
33
+ ("editor_access", "Can edit this app"),
34
+ ),
35
+ "managed": False,
36
+ "default_permissions": (),
37
+ },
38
+ ),
39
+ migrations.CreateModel(
40
+ name="Log",
41
+ fields=[
42
+ (
43
+ "id",
44
+ models.AutoField(
45
+ auto_created=True,
46
+ primary_key=True,
47
+ serialize=False,
48
+ verbose_name="ID",
49
+ ),
50
+ ),
51
+ ("user", models.TextField(blank=True, default="", null=True)),
52
+ ("action", models.TextField(blank=True, default="", null=True)),
53
+ ("details", models.TextField(blank=True, default="", null=True)),
54
+ ("created", models.DateTimeField(auto_now_add=True)),
55
+ ],
56
+ ),
57
+ migrations.CreateModel(
58
+ name="Page",
59
+ fields=[
60
+ (
61
+ "id",
62
+ models.AutoField(
63
+ auto_created=True,
64
+ primary_key=True,
65
+ serialize=False,
66
+ verbose_name="ID",
67
+ ),
68
+ ),
69
+ (
70
+ "title",
71
+ models.CharField(
72
+ help_text="Required: Displayed above content and in menu",
73
+ max_length=255,
74
+ ),
75
+ ),
76
+ (
77
+ "slug",
78
+ models.CharField(
79
+ help_text="Required: Indentifier for path and links",
80
+ max_length=255,
81
+ ),
82
+ ),
83
+ (
84
+ "priority",
85
+ models.IntegerField(
86
+ default=10,
87
+ help_text="Required: Sort order for pages on the same path",
88
+ ),
89
+ ),
90
+ ("content", models.TextField(blank=True, default="", null=True)),
91
+ (
92
+ "groups",
93
+ models.ManyToManyField(
94
+ blank=True,
95
+ default=None,
96
+ help_text="Optional: Restrict Page to specific Groups",
97
+ related_name="+",
98
+ to="auth.group",
99
+ verbose_name="groups",
100
+ ),
101
+ ),
102
+ (
103
+ "parent",
104
+ models.ForeignKey(
105
+ blank=True,
106
+ help_text="Required: Oragnise page hierarchy",
107
+ null=True,
108
+ on_delete=django.db.models.deletion.PROTECT,
109
+ related_name="children",
110
+ to="evewiki.page",
111
+ ),
112
+ ),
113
+ (
114
+ "states",
115
+ models.ManyToManyField(
116
+ blank=True,
117
+ default=None,
118
+ help_text="Optional: Restrict Page to specific States",
119
+ related_name="+",
120
+ to="authentication.state",
121
+ verbose_name="states",
122
+ ),
123
+ ),
124
+ ],
125
+ ),
126
+ migrations.CreateModel(
127
+ name="Setting",
128
+ fields=[
129
+ (
130
+ "id",
131
+ models.AutoField(
132
+ auto_created=True,
133
+ primary_key=True,
134
+ serialize=False,
135
+ verbose_name="ID",
136
+ ),
137
+ ),
138
+ (
139
+ "hierarchy_max_display_depth",
140
+ models.IntegerField(
141
+ default=10,
142
+ help_text="Limit the depth of the tree for the hierarchy on the main display",
143
+ ),
144
+ ),
145
+ (
146
+ "max_versions",
147
+ models.IntegerField(
148
+ default=1000,
149
+ help_text="No one has infinite disk space, a sensible limit which can be modified to clear down the history",
150
+ ),
151
+ ),
152
+ ],
153
+ ),
154
+ migrations.CreateModel(
155
+ name="PageVersion",
156
+ fields=[
157
+ (
158
+ "id",
159
+ models.AutoField(
160
+ auto_created=True,
161
+ primary_key=True,
162
+ serialize=False,
163
+ verbose_name="ID",
164
+ ),
165
+ ),
166
+ ("content", models.TextField(blank=True, default="", null=True)),
167
+ ("created", models.DateTimeField(auto_now_add=True)),
168
+ (
169
+ "page",
170
+ models.ForeignKey(
171
+ blank=True,
172
+ help_text="Page being edited",
173
+ null=True,
174
+ on_delete=django.db.models.deletion.SET_NULL,
175
+ to="evewiki.page",
176
+ ),
177
+ ),
178
+ (
179
+ "user",
180
+ models.ForeignKey(
181
+ blank=True,
182
+ help_text="User editing the page",
183
+ null=True,
184
+ on_delete=django.db.models.deletion.SET_NULL,
185
+ to="authentication.userprofile",
186
+ ),
187
+ ),
188
+ ],
189
+ ),
190
+ ]
File without changes
File without changes
evewiki/models/logs.py ADDED
@@ -0,0 +1,15 @@
1
+ from django.db import models
2
+
3
+
4
+ class Log(models.Model):
5
+ """
6
+ No relationships just a log.
7
+ """
8
+
9
+ user = models.TextField(default="", blank=True, null=True)
10
+
11
+ action = models.TextField(default="", blank=True, null=True)
12
+
13
+ details = models.TextField(default="", blank=True, null=True)
14
+
15
+ created = models.DateTimeField(auto_now_add=True)
@@ -0,0 +1,48 @@
1
+ from django.db import models
2
+
3
+ from allianceauth.authentication.models import UserProfile
4
+
5
+ from .pages import Page
6
+ from .settings import Setting
7
+
8
+
9
+ class PageVersion(models.Model):
10
+ """
11
+ Previous edits have value
12
+ """
13
+
14
+ page = models.ForeignKey(
15
+ Page,
16
+ on_delete=models.SET_NULL,
17
+ null=True,
18
+ blank=True,
19
+ help_text="Page being edited",
20
+ )
21
+
22
+ user = models.ForeignKey(
23
+ UserProfile,
24
+ on_delete=models.SET_NULL,
25
+ null=True,
26
+ blank=True,
27
+ help_text="User editing the page",
28
+ )
29
+
30
+ content = models.TextField(default="", blank=True, null=True)
31
+
32
+ created = models.DateTimeField(auto_now_add=True)
33
+
34
+ def save(self, *args, **kwargs):
35
+ """
36
+ Clean up old versions when saving
37
+ """
38
+ super().save(*args, **kwargs)
39
+ if self.page_id:
40
+ # Get the IDs of old versions to delete
41
+ settings = Setting.get_settings()
42
+ old_version_ids = list(
43
+ PageVersion.objects.filter(page=self.page)
44
+ .order_by("-created")
45
+ .values_list("id", flat=True)[settings.max_versions :]
46
+ )
47
+ if old_version_ids:
48
+ PageVersion.objects.filter(id__in=old_version_ids).delete()
@@ -0,0 +1,249 @@
1
+ import re
2
+ from html import escape
3
+
4
+ from django.contrib.auth.models import Group
5
+ from django.core.exceptions import ValidationError
6
+ from django.db import models
7
+
8
+ from allianceauth.authentication.models import State
9
+
10
+
11
+ class General(models.Model):
12
+ """A meta model for app permissions."""
13
+
14
+ class Meta:
15
+ managed = False
16
+ default_permissions = ()
17
+ permissions = (
18
+ ("basic_access", "Can access this app"),
19
+ ("editor_access", "Can edit this app"),
20
+ )
21
+
22
+
23
+ class Page(models.Model):
24
+ """
25
+ Pages in a hierarchy, identifiable by slugs with a light touch of folder sorting.
26
+ """
27
+
28
+ title = models.CharField(
29
+ max_length=255,
30
+ null=False,
31
+ help_text="Required: Displayed above content and in menu",
32
+ )
33
+
34
+ parent = models.ForeignKey(
35
+ "self",
36
+ on_delete=models.PROTECT,
37
+ related_name="children",
38
+ null=True,
39
+ blank=True,
40
+ help_text="Required: Oragnise page hierarchy",
41
+ )
42
+
43
+ slug = models.CharField(
44
+ max_length=255, help_text="Required: Indentifier for path and links"
45
+ )
46
+
47
+ priority = models.IntegerField(
48
+ default=10, help_text="Required: Sort order for pages on the same path"
49
+ )
50
+
51
+ states = models.ManyToManyField(
52
+ State,
53
+ default=None,
54
+ blank=True,
55
+ related_name="+",
56
+ verbose_name=("states"),
57
+ help_text="Optional: Restrict Page to specific States",
58
+ )
59
+
60
+ groups = models.ManyToManyField(
61
+ Group,
62
+ default=None,
63
+ blank=True,
64
+ related_name="+",
65
+ verbose_name=("groups"),
66
+ help_text="Optional: Restrict Page to specific Groups",
67
+ )
68
+
69
+ content = models.TextField(default="", blank=True, null=True)
70
+
71
+ def __str__(self):
72
+ return self.title
73
+
74
+ def clean(self):
75
+
76
+ # Force slugs to be alphanumber lowercase with dashes
77
+ self.slug = self.slug.lower().replace(" ", "-")
78
+ self.slug = re.sub(r"[^a-z0-9\-]", "", self.slug)
79
+
80
+ # Check slug is unique for this path
81
+ # Prepare the queryset for siblings (excluding self if updating)
82
+ siblings_qs = Page.objects.filter(parent=self.parent)
83
+ if self.pk is not None:
84
+ siblings_qs = siblings_qs.exclude(pk=self.pk)
85
+
86
+ sibling_slugs = list(siblings_qs.values_list("slug", flat=True))
87
+ if self.slug in sibling_slugs:
88
+ raise ValidationError(
89
+ f'A slug with "{self.slug}" already exists ar this path'
90
+ )
91
+
92
+ # Check circular reference to self
93
+ if self.parent and self.pk and self.parent.pk == self.pk:
94
+ raise ValidationError("A page cannot be its own parent.")
95
+
96
+ @property
97
+ def path(self):
98
+ segments = []
99
+ node = self
100
+ while node is not None:
101
+ segments.append(node.slug)
102
+ node = node.parent
103
+ return "/".join(reversed(segments))
104
+
105
+ @property
106
+ def summary(self):
107
+ """
108
+ Generates a summary of headers from Markdown text as a nested HTML list of links.
109
+ """
110
+ header_pattern = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
111
+ headers = []
112
+
113
+ for match in header_pattern.finditer(self.content):
114
+ hashes, title = match.groups()
115
+ level = len(hashes)
116
+ anchor = re.sub(r"[^\w\s-]", "", title).strip().lower()
117
+ anchor = re.sub(r"\s+", "-", anchor)
118
+ headers.append((level, title, anchor))
119
+
120
+ # Build nested HTML list
121
+ html = []
122
+ prev_level = 0
123
+ for level, title, anchor in headers:
124
+ while prev_level < level:
125
+ html.append("<ul>")
126
+ prev_level += 1
127
+ while prev_level > level:
128
+ html.append("</ul>")
129
+ prev_level -= 1
130
+ html.append(f'<li><a href="#{escape(anchor)}">{escape(title)}</a></li>')
131
+ while prev_level > 0:
132
+ html.append("</ul>")
133
+ prev_level -= 1
134
+
135
+ return "\n".join(html)
136
+
137
+ @classmethod
138
+ def list(cls, pages=None, parent=None, level=0):
139
+ """
140
+ Returns a flat list of pages with titles indented by hierarchy level.
141
+ Returns: list of tuple
142
+ """
143
+ if pages is None:
144
+ pages = list(cls.objects.all().select_related("parent"))
145
+
146
+ flat = []
147
+ children = [p for p in pages if p.parent_id == (parent.id if parent else None)]
148
+ children.sort(key=lambda p: p.priority)
149
+
150
+ for child in children:
151
+ indent = "\u2003" * level
152
+ display = f"{indent}{child.title} [/{child.path}]"
153
+ flat.append((child.id, display))
154
+ flat.extend(cls.list(pages, child, level + 1))
155
+
156
+ if parent is None and level == 0:
157
+ flat.insert(0, ("", "/"))
158
+
159
+ return flat
160
+
161
+ @classmethod
162
+ def tree(cls, user=None, pages=None, parent=None, depth=0):
163
+ """
164
+ Recursively builds a tree of pages as nested dicts,
165
+ ordered by priority, with 'path' property.
166
+ Returns: List[Dict]
167
+ """
168
+ if pages is None:
169
+ pages = list(cls.objects.all().select_related("parent"))
170
+
171
+ tree = []
172
+ children = [p for p in pages if p.parent_id == (parent.id if parent else None)]
173
+ children.sort(key=lambda p: p.priority)
174
+
175
+ for child in children:
176
+ node = {
177
+ "id": child.id,
178
+ "title": child.title,
179
+ "slug": child.slug,
180
+ "priority": child.priority,
181
+ "path": getattr(child, "path", None),
182
+ "depth": depth,
183
+ "children": cls.tree(user, pages, child, depth=depth + 1),
184
+ }
185
+ if child.user_access(user=user):
186
+ tree.append(node)
187
+ return tree
188
+
189
+ @classmethod
190
+ def get_by_path(cls, path: str, user):
191
+ """
192
+ Find an id for a given path
193
+ Returns: Page id or None
194
+ """
195
+ path = path.split("/")
196
+ parent = None
197
+ page = None
198
+ for slug in path:
199
+ try:
200
+ page = cls.objects.filter(slug=slug, parent=parent).first()
201
+ except cls.DoesNotExist:
202
+ return None
203
+ parent = page
204
+
205
+ if page and not page.user_access(user=user):
206
+ return None
207
+
208
+ return page if page else None
209
+
210
+ def user_access(self, user):
211
+
212
+ # Admin has full access
213
+ if user.profile.state.name == "Admin":
214
+ return True
215
+
216
+ # Editor has full access
217
+ if user.has_perm("evewiki.editor_access"):
218
+ return True
219
+
220
+ access = False
221
+
222
+ # If no groups or states assigned then yes
223
+ groups_empty = not self.groups.exists()
224
+ states_empty = not self.states.exists()
225
+ if groups_empty and states_empty:
226
+ access = True
227
+
228
+ # If any of the user's groups are allowed access
229
+ user_has_group = False
230
+ if any(
231
+ group in self.groups.all()
232
+ for group in user.groups.values_list("name", flat=True)
233
+ ):
234
+ access = True
235
+ user_has_group = True
236
+
237
+ # If the user's states is in the states list
238
+ user_has_state = False
239
+ if self.states.filter(name=user.profile.state.name).exists():
240
+ access = True
241
+ user_has_state = True
242
+
243
+ # If Page has both User needs both
244
+ if not groups_empty and not states_empty:
245
+ access = False
246
+ if user_has_group and user_has_state:
247
+ access = True
248
+
249
+ return access
@@ -0,0 +1,35 @@
1
+ from django.db import models
2
+
3
+
4
+ class Setting(models.Model):
5
+ """
6
+ Settings and feature flags.
7
+ """
8
+
9
+ hierarchy_max_display_depth = models.IntegerField(
10
+ default=10,
11
+ null=False,
12
+ help_text="Limit the depth of the tree for the hierarchy on the main display",
13
+ )
14
+
15
+ max_versions = models.IntegerField(
16
+ default=1000,
17
+ null=False,
18
+ help_text="No one has infinite disk space, a sensible limit which can be modified to clear down the history",
19
+ )
20
+
21
+ def get_settings():
22
+ """
23
+ Returns the Setting instance with the lowest id,
24
+ or a default Setting instance (not saved) if none exist.
25
+ """
26
+ setting = Setting.objects.order_by("id").first()
27
+ if setting is not None:
28
+ return setting
29
+
30
+ return Setting(
31
+ hierarchy_max_display_depth=Setting._meta.get_field(
32
+ "hierarchy_max_display_depth"
33
+ ).get_default(),
34
+ max_versions=Setting._meta.get_field("max_versions").get_default(),
35
+ )
evewiki/tasks.py ADDED
@@ -0,0 +1,12 @@
1
+ """Tasks."""
2
+
3
+ from celery import shared_task
4
+
5
+ from allianceauth.services.hooks import get_extension_logger
6
+
7
+ logger = get_extension_logger(__name__)
8
+
9
+
10
+ @shared_task
11
+ def my_task():
12
+ """An example task."""
@@ -0,0 +1,11 @@
1
+ {% extends 'allianceauth/base-bs5.html' %}
2
+ {% load i18n %}
3
+
4
+ {% block page_title %}{% translate "wiki" %}{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="allianceauth-evewiki-plugin">
8
+
9
+ {% block details %}{% endblock %}
10
+ </div>
11
+ {% endblock %}
@@ -0,0 +1,16 @@
1
+ {% block details %}
2
+ <h1>Wiki</h1>
3
+ Use the `+` icon to add a new Page.<br />
4
+ On the left; the menu/hierarchy will appear as pages are added.<br />
5
+ On the right; details/options for a page will become available as you navigate the hierarchy.
6
+
7
+ {% endblock %}
8
+
9
+ {% block extra_javascript %}
10
+ {% endblock %}
11
+
12
+ {% block extra_css %}
13
+ {% endblock %}
14
+
15
+ {% block extra_script %}
16
+ {% endblock %}