##// END OF EJS Templates
Added new markdown field. Added gets algorithm with regexes.
neko259 -
r39:e544cbe2 default
parent child Browse files
Show More
@@ -1,214 +1,212 b''
1 1 import os
2 2 import re
3 3 from django.db import models
4 4 from django.utils import timezone
5 5 import time
6 6
7 7 from neboard import settings
8 from markupfield.fields import MarkupField
8 9
9 10 import thumbs
10 11
11 12 NO_PARENT = -1
12 13 NO_IP = '0.0.0.0'
13 14 UNKNOWN_UA = ''
14 15
15 16
16 17 def update_image_filename(instance, filename):
17 18 """Get unique image filename"""
18 19
19 20 path = 'images/'
20 21 new_name = str(int(time.mktime(time.gmtime()))) + '_' + filename
21 22 return os.path.join(path, new_name)
22 23
23 24
24 25 class PostManager(models.Manager):
25 26 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
26 27 ip=NO_IP, tags=None):
27 28 post = self.create(title=title,
28 29 text=text,
29 30 pub_time=timezone.now(),
30 31 parent=parent_id,
31 32 image=image,
32 33 poster_ip=ip,
33 34 poster_user_agent=UNKNOWN_UA,
34 35 last_edit_time=timezone.now())
35 36
36 37 if tags:
37 38 for tag in tags:
38 39 post.tags.add(tag)
39 40
40 41 if parent_id != NO_PARENT:
41 42 self._bump_thread(parent_id)
42 43 else:
43 44 self._delete_old_threads()
44 45
45 46 return post
46 47
47 48 def delete_post(self, post):
48 49 children = self.filter(parent=post.id)
49 50 for child in children:
50 51 self.delete_post(child)
51 52 post.delete()
52 53
53 54 def delete_posts_by_ip(self, ip):
54 55 posts = self.filter(poster_ip=ip)
55 56 for post in posts:
56 57 self.delete_post(post)
57 58
58 59 def get_threads(self, tag=None):
59 60 if tag:
60 61 threads = self.filter(parent=NO_PARENT, tags=tag)
61 62 else:
62 63 threads = self.filter(parent=NO_PARENT)
63 64 threads = list(threads.order_by('-last_edit_time'))
64 65
65 66 return threads
66 67
67 68 def get_thread(self, opening_post_id):
68 69 opening_post = self.get(id=opening_post_id)
69 70
70 71 if opening_post.parent == NO_PARENT:
71 72 replies = self.filter(parent=opening_post_id)
72 73
73 74 thread = [opening_post]
74 75 thread.extend(replies)
75 76
76 77 return thread
77 78
78 79 def exists(self, post_id):
79 80 posts = self.filter(id=post_id)
80 81
81 82 return len(posts) > 0
82 83
83 84 def _delete_old_threads(self):
84 85 """
85 86 Preserves maximum thread count. If there are too many threads,
86 87 delete the old ones.
87 88 """
88 89
89 90 # TODO Try to find a better way to get the active thread count.
90 91
91 92 # TODO Move old threads to the archive instead of deleting them.
92 93 # Maybe make some 'old' field in the model to indicate the thread
93 94 # must not be shown and be able for replying.
94 95
95 96 threads = self.get_threads()
96 97 thread_count = len(threads)
97 98
98 99 if thread_count > settings.MAX_THREAD_COUNT:
99 100 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
100 101 old_threads = threads[-num_threads_to_delete:]
101 102
102 103 for thread in old_threads:
103 104 self.delete_post(thread)
104 105
105 106 def _bump_thread(self, thread_id):
106 107 thread = self.get(id=thread_id)
107 108
108 109 replies_count = len(self.get_thread(thread_id))
109 110 if replies_count <= settings.MAX_POSTS_PER_THREAD:
110 111 thread.last_edit_time = timezone.now()
111 112 thread.save()
112 113
113 114
114 115 class TagManager(models.Manager):
115 116 def get_not_empty_tags(self):
116 117 all_tags = self.all().order_by('name')
117 118 tags = []
118 119 for tag in all_tags:
119 120 if not tag.is_empty():
120 121 tags.append(tag)
121 122
122 123 return tags
123 124
124 125
125 126 class Tag(models.Model):
126 127 """
127 128 A tag is a text node assigned to the post. The tag serves as a board
128 129 section. There can be multiple tags for each message
129 130 """
130 131
131 132 objects = TagManager()
132 133
133 134 name = models.CharField(max_length=100)
134 135 # TODO Connect the tag to its posts to check the number of threads for
135 136 # the tag.
136 137
137 138 def __unicode__(self):
138 139 return self.name
139 140
140 141 def is_empty(self):
141 142 return self.get_post_count() == 0
142 143
143 144 def get_post_count(self):
144 145 posts_with_tag = Post.objects.get_threads(tag=self)
145 146 return len(posts_with_tag)
146 147
147 148
148 149 class Post(models.Model):
149 150 """A post is a message."""
150 151
151 152 objects = PostManager()
152 153
153 154 title = models.CharField(max_length=50)
154 155 pub_time = models.DateTimeField()
155 text = models.TextField()
156 text = MarkupField(default_markup_type='markdown', escape_html=True)
156 157 image = thumbs.ImageWithThumbsField(upload_to=update_image_filename,
157 158 blank=True, sizes=((200, 150),))
158 159 poster_ip = models.IPAddressField()
159 160 poster_user_agent = models.TextField()
160 161 parent = models.BigIntegerField()
161 162 tags = models.ManyToManyField(Tag)
162 163 last_edit_time = models.DateTimeField()
163 164
164 165 def __unicode__(self):
165 166 return self.title + ' (' + self.text + ')'
166 167
167 168 def _get_replies(self):
168 169 return Post.objects.filter(parent=self.id)
169 170
170 171 def get_reply_count(self):
171 172 return len(self._get_replies())
172 173
173 174 def get_images_count(self):
174 175 images_count = 1 if self.image else 0
175 176 for reply in self._get_replies():
176 177 if reply.image:
177 178 images_count += 1
178 179
179 180 return images_count
180 181
181 182 def get_gets_count(self):
182 183 gets_count = 1 if self.is_get() else 0
183 184 for reply in self._get_replies():
184 185 if reply.is_get():
185 186 gets_count += 1
186 187
187 188 return gets_count
188 189
189 190 def is_get(self):
190 191 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
191 192
192 # TODO Make a better algorithm for describing gets
193 return self.id == 1 or self.id % 10 == 0
193 first = self.id == 1
194 194
195 def get_parsed_text(self):
196 text = self.text
195 # TODO Compile regexes
197 196
198 # TODO Implement parsing
199 # Make //text colored
200 # Make >>12 links to posts
201
202 return text
197 id_str = str(self.id)
198 pretty = re.match(r'\d0+', id_str)
199 same_digits = re.match(r'^(.)\1{1,}$', id_str)
200 return first or pretty or same_digits
203 201
204 202
205 203 class Admin(models.Model):
206 204 """
207 205 Model for admin users
208 206 """
209 207 name = models.CharField(max_length=100)
210 208 password = models.CharField(max_length=100)
211 209
212 210 def __unicode__(self):
213 211 return self.name + '/' + '*' * len(self.password)
214 212
@@ -1,152 +1,149 b''
1 1 html {
2 2 background: #555;
3 3 color: #ffffff;
4 4 }
5 5
6 6 #admin_panel {
7 7 background: #FF0000;
8 8 color: #00FF00
9 9 }
10 10
11 11 .title {
12 12 font-weight: bold;
13 13 color: #ffcc00;
14 14 }
15 15
16 16 .link, a {
17 17 color: #afdcec;
18 18 }
19 19
20 20 .link:hover {
21 21 color: #fff380;
22 22 }
23 23
24 24 .post_id {
25 25 color: #ffffff;
26 26 }
27 27
28 .block {
29 display: inline-block;
30 vertical-align: top;
31 }
32
33 28 .tag {
34 29 color: #b4cfec;
35 30 }
36 31
37 32 .tag:hover {
38 33 color: #d0edb4;
39 34 }
40 35
41 36 .post_id {
42 37 color: #fff380;
43 38 }
44 39
45 40 .post {
46 41 background: #333;
47 42 margin: 5px;
48 43 padding: 10px;
49 44 border-radius: 5px;
45 clear: left;
50 46 }
51 47
52 48 .metadata {
53 49 padding: 2px;
54 50 margin-top: 10px;
55 51 border: solid 1px #666;
56 52 font-size: 0.9em;
57 color: #ddd
53 color: #ddd;
58 54 }
59 55
60 56 #navigation_panel {
61 57 background: #444;
62 58 margin: 5px;
63 59 padding: 10px;
64 60 border-radius: 5px;
65 61 color: #eee;
66 62 }
67 63
68 64 #navigation_panel .link {
69 65 border-right: 1px solid #fff;
70 66 font-weight: bold;
71 67 margin-right: 1ex;
72 68 padding-right: 1ex;
73 69 }
74 70 #navigation_panel .link:last-child {
75 71 border-left: 1px solid #fff;
76 72 border-right: none;
77 73 float: right;
78 74 margin-left: 1ex;
79 75 margin-right: 0;
80 76 padding-left: 1ex;
81 77 padding-right: 0;
82 78 }
83 79
84 #navigation_panel::after {
80 #navigation_panel::after, .post::after {
85 81 clear: both;
86 82 content: ".";
87 83 display: block;
88 84 height: 0;
89 85 line-height: 0;
90 86 visibility: hidden;
91 87 }
92 88
93 89 p {
94 90 margin-top: .5em;
95 91 margin-bottom: .5em;
96 92 }
97 93
98 94 .post-form-w {
99 95 display: table;
100 96 background: #333344;
101 97 border-radius: 5px;
102 98 color: #fff;
103 99 padding: 10px;
104 100 margin: 5px
105 101 }
106 102
107 103 .form-row {
108 104 display: table-row;
109 105 }
110 106
111 107 .form-label, .form-input {
112 108 display: table-cell;
113 109 }
114 110
115 111 .form-label {
116 112 padding: .25em 1ex .25em 0;
113 vertical-align: top;
117 114 }
118 115
119 116 .form-input {
120 117 padding: .25em 0;
121 118 }
122 119
123 120 .post-form input, .post-form textarea {
124 121 background: #333;
125 122 color: #fff;
126 123 border: solid 1px;
127 124 padding: 0;
128 125 width: 100%;
129 126 }
130 127
131 128 .form-submit {
132 129 border-bottom: 2px solid #ddd;
133 130 margin-bottom: .5em;
134 131 padding-bottom: .5em;
135 132 }
136 133
137 134 .form-title {
138 135 font-weight: bold;
139 136 }
140 137
141 138 input[type="submit"] {
142 139 background: #222;
143 140 border: solid 1px #fff;
144 141 color: #fff;
145 142 }
146 143
147 144 blockquote {
148 145 border-left: solid 2px;
149 146 padding-left: 5px;
150 147 color: #B1FB17;
151 148 margin: 0;
152 149 } No newline at end of file
@@ -1,177 +1,178 b''
1 1 # Django settings for neboard project.
2 2 import os
3 3
4 4 DEBUG = True
5 5 TEMPLATE_DEBUG = DEBUG
6 6
7 7 ADMINS = (
8 8 # ('Your Name', 'your_email@example.com'),
9 9 ('admin', 'admin@example.com')
10 10 )
11 11
12 12 MANAGERS = ADMINS
13 13
14 14 DATABASES = {
15 15 'default': {
16 16 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
17 17 'NAME': 'database.db', # Or path to database file if using sqlite3.
18 18 'USER': '', # Not used with sqlite3.
19 19 'PASSWORD': '', # Not used with sqlite3.
20 20 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
21 21 'PORT': '', # Set to empty string for default. Not used with sqlite3.
22 22 }
23 23 }
24 24
25 25 # Local time zone for this installation. Choices can be found here:
26 26 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
27 27 # although not all choices may be available on all operating systems.
28 28 # In a Windows environment this must be set to your system time zone.
29 29 TIME_ZONE = 'Europe/Kiev'
30 30
31 31 # Language code for this installation. All choices can be found here:
32 32 # http://www.i18nguy.com/unicode/language-identifiers.html
33 33 LANGUAGE_CODE = 'ru'
34 34
35 35 SITE_ID = 1
36 36
37 37 # If you set this to False, Django will make some optimizations so as not
38 38 # to load the internationalization machinery.
39 39 USE_I18N = True
40 40
41 41 # If you set this to False, Django will not format dates, numbers and
42 42 # calendars according to the current locale.
43 43 USE_L10N = True
44 44
45 45 # If you set this to False, Django will not use timezone-aware datetimes.
46 46 USE_TZ = True
47 47
48 48 # Absolute filesystem path to the directory that will hold user-uploaded files.
49 49 # Example: "/home/media/media.lawrence.com/media/"
50 50 MEDIA_ROOT = ''
51 51
52 52 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
53 53 # trailing slash.
54 54 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
55 55 MEDIA_URL = ''
56 56
57 57 # Absolute path to the directory static files should be collected to.
58 58 # Don't put anything in this directory yourself; store your static files
59 59 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
60 60 # Example: "/home/media/media.lawrence.com/static/"
61 61 STATIC_ROOT = ''
62 62
63 63 # URL prefix for static files.
64 64 # Example: "http://media.lawrence.com/static/"
65 65 STATIC_URL = '/static/'
66 66
67 67 # Additional locations of static files
68 68 # It is really a hack, put real paths, not related
69 69 STATICFILES_DIRS = (
70 70 os.path.dirname(__file__) + '/boards/static',
71 71
72 72 # '/d/work/python/django/neboard/neboard/boards/static',
73 73 # Put strings here, like "/home/html/static" or "C:/www/django/static".
74 74 # Always use forward slashes, even on Windows.
75 75 # Don't forget to use absolute paths, not relative paths.
76 76 )
77 77
78 78 # List of finder classes that know how to find static files in
79 79 # various locations.
80 80 STATICFILES_FINDERS = (
81 81 'django.contrib.staticfiles.finders.FileSystemFinder',
82 82 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
83 83 # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
84 84 )
85 85
86 86 # Make this unique, and don't share it with anybody.
87 87 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
88 88
89 89 # List of callables that know how to import templates from various sources.
90 90 TEMPLATE_LOADERS = (
91 91 'django.template.loaders.filesystem.Loader',
92 92 'django.template.loaders.app_directories.Loader',
93 93 # 'django.template.loaders.eggs.Loader',
94 94 )
95 95
96 96 TEMPLATE_CONTEXT_PROCESSORS = (
97 97 'django.core.context_processors.media',
98 98 'django.core.context_processors.static',
99 99 'django.core.context_processors.request',
100 100 'django.contrib.auth.context_processors.auth',
101 101 )
102 102
103 103 MIDDLEWARE_CLASSES = (
104 104 'django.middleware.common.CommonMiddleware',
105 105 'django.contrib.sessions.middleware.SessionMiddleware',
106 106 # 'django.middleware.csrf.CsrfViewMiddleware',
107 107 'django.contrib.auth.middleware.AuthenticationMiddleware',
108 108 'django.contrib.messages.middleware.MessageMiddleware',
109 109 # Uncomment the next line for simple clickjacking protection:
110 110 # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
111 111 )
112 112
113 113 ROOT_URLCONF = 'neboard.urls'
114 114
115 115 # Python dotted path to the WSGI application used by Django's runserver.
116 116 WSGI_APPLICATION = 'neboard.wsgi.application'
117 117
118 118 TEMPLATE_DIRS = (
119 119 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
120 120 # Always use forward slashes, even on Windows.
121 121 # Don't forget to use absolute paths, not relative paths.
122 122 'templates',
123 123 )
124 124
125 125 INSTALLED_APPS = (
126 126 'django.contrib.auth',
127 127 'django.contrib.contenttypes',
128 128 'django.contrib.sessions',
129 129 'django.contrib.sites',
130 130 'django.contrib.messages',
131 131 'django.contrib.staticfiles',
132 132 # Uncomment the next line to enable the admin:
133 133 'django.contrib.admin',
134 134 # Uncomment the next line to enable admin documentation:
135 135 # 'django.contrib.admindocs',
136 136 'django.contrib.markup',
137 'django_cleanup',
137 138 'boards',
138 139 )
139 140
140 141 # A sample logging configuration. The only tangible logging
141 142 # performed by this configuration is to send an email to
142 143 # the site admins on every HTTP 500 error when DEBUG=False.
143 144 # See http://docs.djangoproject.com/en/dev/topics/logging for
144 145 # more details on how to customize your logging configuration.
145 146 LOGGING = {
146 147 'version': 1,
147 148 'disable_existing_loggers': False,
148 149 'filters': {
149 150 'require_debug_false': {
150 151 '()': 'django.utils.log.RequireDebugFalse'
151 152 }
152 153 },
153 154 'handlers': {
154 155 'mail_admins': {
155 156 'level': 'ERROR',
156 157 'filters': ['require_debug_false'],
157 158 'class': 'django.utils.log.AdminEmailHandler'
158 159 }
159 160 },
160 161 'loggers': {
161 162 'django.request': {
162 163 'handlers': ['mail_admins'],
163 164 'level': 'ERROR',
164 165 'propagate': True,
165 166 },
166 167 }
167 168 }
168 169
169 170 # Custom imageboard settings
170 171 MAX_POSTS_PER_THREAD = 500 # Thread bumplimit
171 172 MAX_THREAD_COUNT = 20 # Old threads will be deleted to preserve this count
172 173 SITE_NAME = 'Neboard'
173 174
174 175 THEMES = [
175 176 ('md', 'Mystic Dark'),
176 177 ('sw', 'Snow White') ]
177 178 DEFAULT_THEME = 'md' No newline at end of file
@@ -1,82 +1,84 b''
1 1 {% extends "base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load markup %}
5 5
6 6 {% block head %}
7 7 <title>Neboard</title>
8 8 {% endblock %}
9 9
10 10 {% block content %}
11 11
12 12 {% if threads %}
13 13 {% for thread in threads %}
14 14 <div class="post">
15 15 {% if thread.image %}
16 <div class="block">
16 <div class="image">
17 17 <a href="{{ thread.image.url }}"><img
18 18 src="{{ thread.image.url_200x150 }}" />
19 19 </a>
20 20 </div>
21 21 {% endif %}
22 <div class="block">
22 <div class="message">
23 23 <span class="title">{{ thread.title }}</span>
24 24 <span class="post_id">(#{{ thread.id }})</span>
25 25 [{{ thread.pub_time }}]
26 26 [<a class="link" href="{% url 'thread' thread.id %}"
27 27 >{% trans "View" %}</a>]
28 {{ thread.get_parsed_text|markdown:"safe"|truncatechars:300 }}
28 {% autoescape off %}
29 {{ thread.text.rendered|truncatechars:300 }}
30 {% endautoescape %}
29 31 </div>
30 32 <div class="metadata">
31 33 {{ thread.get_reply_count }} {% trans 'replies' %},
32 34 {{ thread.get_images_count }} {% trans 'images' %},
33 35 {{ thread.get_gets_count }} {% trans 'gets' %}.
34 36 {% if thread.tags.all %}
35 37 <span class="tags">{% trans 'Tags' %}:
36 38 {% for tag in thread.tags.all %}
37 39 <a class="tag" href="
38 40 {% url 'tag' tag_name=tag.name %}">
39 41 {{ tag.name }}</a>
40 42 {% endfor %}
41 43 </span>
42 44 {% endif %}
43 45 </div>
44 46 </div>
45 47 {% endfor %}
46 48 {% else %}
47 49 No threads found.
48 50 <hr />
49 51 {% endif %}
50 52
51 53 <form enctype="multipart/form-data" method="post">{% csrf_token %}
52 54 <div class="post-form-w">
53 55 <div class="form-title">{% trans "Create new thread" %}</div>
54 56 <div class="post-form">
55 57 <div class="form-row">
56 58 <div class="form-label">{% trans 'Title' %}</div>
57 59 <div class="form-input">{{ form.title }}</div>
58 60 </div>
59 61 <div class="form-row">
60 62 <div class="form-label">{% trans 'Text' %}</div>
61 63 <div class="form-input">{{ form.text }}</div>
62 64 </div>
63 65 <div class="form-row">
64 66 <div class="form-label">{% trans 'Image' %}</div>
65 67 <div class="form-input">{{ form.image }}</div>
66 68 </div>
67 69 <div class="form-row">
68 70 <div class="form-label">{% trans 'Tags' %}</div>
69 71 <div class="form-input">{{ form.tags }}</div>
70 72 </div>
71 73 </div>
72 74 <div class="form-submit"><input type="submit"
73 75 value="{% trans "Post" %}"/></div>
74 76 <div>Tags must be delimited by spaces. Text or image is required
75 77 </div>
76 78 <div>Use <a
77 79 href="http://daringfireball.net/projects/markdown/basics">
78 80 markdown</a> syntax for posting.</div>
79 81 </div>
80 82 </form>
81 83
82 84 {% endblock %} No newline at end of file
@@ -1,74 +1,76 b''
1 1 {% extends "base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load markup %}
5 5
6 6 {% block head %}
7 7 <title>Neboard</title>
8 8 {% endblock %}
9 9
10 10 {% block content %}
11 11
12 12 {% if posts %}
13 13 {% for post in posts %}
14 14 <a name="{{ post.id }}"></a>
15 15 <div class="post">
16 16 {% if post.image %}
17 17 <div class="block">
18 18 <a href="{{ post.image.url }}"><img
19 19 src="{{ post.image.url_200x150 }}" />
20 20 </a>
21 21 </div>
22 22 {% endif %}
23 23 <div class="block">
24 24 <span class="title">{{ post.title }}</span>
25 25 <a class="post_id" href="#{{ post.id }}">
26 26 (#{{ post.id }})</a>
27 27 [{{ post.pub_time }}]
28 {{ post.get_parsed_text|markdown:"safe" }}
28 {% autoescape off %}
29 {{ post.text.rendered }}
30 {% endautoescape %}
29 31 </div>
30 32 {% if post.tags.all %}
31 33 <div class="metadata">
32 34 <span class="tags">{% trans 'Tags' %}:
33 35 {% for tag in post.tags.all %}
34 36 <a class="tag" href="{% url 'tag' tag.name %}">
35 37 {{ tag.name }}</a>
36 38 {% endfor %}
37 39 </span>
38 40 </div>
39 41 {% endif %}
40 42 </div>
41 43 {% endfor %}
42 44 {% else %}
43 45 No threads found.
44 46 <hr />
45 47 {% endif %}
46 48
47 49 <form enctype="multipart/form-data" method="post">{% csrf_token %}
48 50 <div class="post-form-w">
49 51 <div class="form-title">{% trans "Reply to thread" %}</div>
50 52 <div class="post-form">
51 53 <div class="form-row">
52 54 <div class="form-label">{% trans 'Title' %}</div>
53 55 <div class="form-input">{{ form.title }}</div>
54 56 </div>
55 57 <div class="form-row">
56 58 <div class="form-label">{% trans 'Text' %}</div>
57 59 <div class="form-input">{{ form.text }}</div>
58 60 </div>
59 61 <div class="form-row">
60 62 <div class="form-label">{% trans 'Image' %}</div>
61 63 <div class="form-input">{{ form.image }}</div>
62 64 </div>
63 65 </div>
64 66 <div class="form-submit"><input type="submit"
65 67 value="{% trans "Post" %}"/></div>
66 68 <div>Use <a
67 69 href="http://daringfireball.net/projects/markdown/basics">
68 70 markdown</a> syntax for posting.</div>
69 71 <div>Example: *<i>italic</i>*, **<b>bold</b>**</div>
70 72 <div>Insert quotes with "&gt;"</div>
71 73 </div>
72 74 </form>
73 75
74 76 {% endblock %}
General Comments 0
You need to be logged in to leave comments. Login now