Просмотр исходного кода

- Improved user profiles
- Display posts and edit's ip's and useragents

Ralfp 12 лет назад
Родитель
Сommit
dc4f63c0ff

+ 15 - 0
misago/profiles/template.py

@@ -14,6 +14,21 @@ def RequestContext(request, context=None):
     if request.user.is_authenticated() and request.user.pk != context['profile'].pk:
     if request.user.is_authenticated() and request.user.pk != context['profile'].pk:
         context['follows'] = request.user.is_following(context['profile'])
         context['follows'] = request.user.is_following(context['profile'])
         context['ignores'] = request.user.is_ignoring(context['profile'])
         context['ignores'] = request.user.is_ignoring(context['profile'])
+        
+    # Find out if this user allows us to see his activity
+    if request.user.pk != context['profile'].pk:
+        if context['profile'].hide_activity == 2:
+            context['hidden'] = True
+        if context['profile'].hide_activity == 1:
+            context['hidden'] = context['profile'].is_following(request.user)
+    else:
+        context['hidden'] = False
+
+    # Find out if this user is online:
+    if request.user.pk != context['profile'].pk:
+        context['online'] = context['profile'].sessions.count() > 0
+    else:
+        context['online'] = True
 
 
     context['tabs'] = []
     context['tabs'] = []
     for extension in settings.PROFILE_EXTENSIONS:
     for extension in settings.PROFILE_EXTENSIONS:

+ 3 - 1
misago/profiles/views.py

@@ -7,7 +7,7 @@ from misago.profiles.forms import QuickFindUserForm
 from misago.ranks.models import Rank
 from misago.ranks.models import Rank
 from misago.users.models import User
 from misago.users.models import User
 from misago.utils import slugify
 from misago.utils import slugify
-from misago.views import error404
+from misago.views import error403, error404
 
 
 def list(request, rank_slug=None):
 def list(request, rank_slug=None):
     ranks = Rank.objects.filter(as_tab=1).order_by('order')
     ranks = Rank.objects.filter(as_tab=1).order_by('order')
@@ -30,6 +30,8 @@ def list(request, rank_slug=None):
 
 
     # Users search?
     # Users search?
     if request.method == 'POST':
     if request.method == 'POST':
+        if not acl.users.can_search_users():
+            return error403(request)
         in_search = True
         in_search = True
         active_rank = None
         active_rank = None
         search_form = QuickFindUserForm(request.POST, request=request)
         search_form = QuickFindUserForm(request.POST, request=request)

+ 10 - 0
misago/roles/fixtures.py

@@ -11,6 +11,10 @@ def load_fixtures():
                           'can_use_signature': True,
                           'can_use_signature': True,
                           'allow_signature_links': True,
                           'allow_signature_links': True,
                           'allow_signature_images': True,
                           'allow_signature_images': True,
+                          'can_search_users': True,
+                          'can_see_users_emails': True,
+                          'can_see_users_trails': True,
+                          'can_see_hidden_users': True,
                           'forums': {5: 1, 6: 1, 7: 1},
                           'forums': {5: 1, 6: 1, 7: 1},
                           })
                           })
     role.save(force_insert=True)
     role.save(force_insert=True)
@@ -21,6 +25,10 @@ def load_fixtures():
                           'changes_expire': 14,
                           'changes_expire': 14,
                           'can_use_signature': True,
                           'can_use_signature': True,
                           'allow_signature_links': True,
                           'allow_signature_links': True,
+                          'can_search_users': True,
+                          'can_see_users_emails': True,
+                          'can_see_users_trails': True,
+                          'can_see_hidden_users': True,
                           'forums': {5: 1, 6: 1, 7: 1},
                           'forums': {5: 1, 6: 1, 7: 1},
                           })
                           })
     role.save(force_insert=True)
     role.save(force_insert=True)
@@ -29,12 +37,14 @@ def load_fixtures():
     role.set_permissions({
     role.set_permissions({
                           'name_changes_allowed': 2,
                           'name_changes_allowed': 2,
                           'can_use_signature': False,
                           'can_use_signature': False,
+                          'can_search_users': True,
                           'forums': {5: 3, 6: 3, 7: 3},
                           'forums': {5: 3, 6: 3, 7: 3},
                           })
                           })
     role.save(force_insert=True)
     role.save(force_insert=True)
     
     
     role = Role(name=_("Guest").message, token='guest')
     role = Role(name=_("Guest").message, token='guest')
     role.set_permissions({
     role.set_permissions({
+                          'can_search_users': True,
                           'forums': {5: 6, 6: 6, 7: 6},
                           'forums': {5: 6, 6: 6, 7: 6},
                           })
                           })
     role.save(force_insert=True)
     role.save(force_insert=True)

+ 1 - 0
misago/settings_base.py

@@ -103,6 +103,7 @@ MIDDLEWARE_CLASSES = (
 # List of application permission providers
 # List of application permission providers
 PERMISSION_PROVIDERS = (
 PERMISSION_PROVIDERS = (
     'misago.usercp.acl',
     'misago.usercp.acl',
+    'misago.users.acl',
     'misago.admin.acl',
     'misago.admin.acl',
     'misago.forums.acl',
     'misago.forums.acl',
     'misago.threads.acl',
     'misago.threads.acl',

+ 1 - 0
misago/threads/urls.py

@@ -20,6 +20,7 @@ urlpatterns = patterns('misago.threads.views',
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'DeleteView', name="thread_hide", kwargs={'mode': 'hide_thread'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/hide/$', 'DeleteView', name="thread_hide", kwargs={'mode': 'hide_thread'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'DeleteView', name="post_delete", kwargs={'mode': 'delete_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/delete/$', 'DeleteView', name="post_delete", kwargs={'mode': 'delete_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'DeleteView', name="post_hide", kwargs={'mode': 'hide_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/hide/$', 'DeleteView', name="post_hide", kwargs={'mode': 'hide_post'}),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/info/$', 'DetailsView', name="post_info"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'ChangelogView', name="changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'ChangelogView', name="changelog"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'ChangelogDiffView', name="changelog_diff"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'ChangelogDiffView', name="changelog_diff"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'ChangelogRevertView', name="changelog_revert"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'ChangelogRevertView', name="changelog_revert"),

+ 1 - 0
misago/threads/views/__init__.py

@@ -3,5 +3,6 @@ from misago.threads.views.jumps import *
 from misago.threads.views.thread import *
 from misago.threads.views.thread import *
 from misago.threads.views.delete import *
 from misago.threads.views.delete import *
 from misago.threads.views.posting import *
 from misago.threads.views.posting import *
+from misago.threads.views.details import *
 from misago.threads.views.changelog import *
 from misago.threads.views.changelog import *
 
 

+ 42 - 0
misago/threads/views/details.py

@@ -0,0 +1,42 @@
+from django.template import RequestContext
+from misago.acl.utils import ACLError403, ACLError404
+from misago.forums.models import Forum
+from misago.threads.models import Thread, Post
+from misago.threads.views.base import BaseView
+from misago.views import error403, error404
+from misago.utils import make_pagination, slugify
+
+class DetailsView(BaseView):
+    def fetch_target(self, kwargs):
+        self.thread = Thread.objects.get(pk=kwargs['thread'])
+        self.forum = self.thread.forum
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+        self.parents = Forum.objects.forum_parents(self.forum.pk, True)
+        self.post = Post.objects.select_related('user').get(pk=kwargs['post'], thread=self.thread.pk)
+        self.post.thread = self.thread
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+        self.request.acl.users.allow_details_view()
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.forum = None
+        self.thread = None
+        self.post = None
+        try:
+            self.fetch_target(kwargs)
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(request, e.message)
+        except ACLError404 as e:
+            return error404(request, e.message)
+        return request.theme.render_to_response('threads/details.html',
+                                                {
+                                                 'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'thread': self.thread,
+                                                 'post': self.post,
+                                                 },
+                                                context_instance=RequestContext(request))

+ 1 - 1
misago/usercp/acl.py

@@ -13,7 +13,7 @@ def make_form(request, role, form):
         form.base_fields['allow_signature_links'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['allow_signature_links'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['allow_signature_images'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.base_fields['allow_signature_images'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
         form.layout.append((
         form.layout.append((
-                            _("Owned Profile"),
+                            _("Profile Settings"),
                             (
                             (
                              ('name_changes_allowed', {'label': _("Allowed Username changes number"), 'help_text': _("Enter zero to don't allow users with this role to change their names.")}),
                              ('name_changes_allowed', {'label': _("Allowed Username changes number"), 'help_text': _("Enter zero to don't allow users with this role to change their names.")}),
                              ('changes_expire', {'label': _("Don't count username changes older than"), 'help_text': _("Number of days since name change that makes that change no longer count to limit. For example, if you enter 7 days and set changes limit 3, users with this rank will not be able to make more than three changes in duration of 7 days. Enter zero to make all changes count.")}),
                              ('changes_expire', {'label': _("Don't count username changes older than"), 'help_text': _("Number of days since name change that makes that change no longer count to limit. For example, if you enter 7 days and set changes limit 3, users with this rank will not be able to make more than three changes in duration of 7 days. Enter zero to make all changes count.")}),

+ 60 - 0
misago/users/acl.py

@@ -0,0 +1,60 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from misago.acl.builder import BaseACL
+from misago.acl.utils import ACLError404
+from misago.forms import YesNoSwitch
+
+def make_form(request, role, form):
+    form.base_fields['can_search_users'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_users_emails'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_users_trails'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_hidden_users'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    
+    form.layout.append((
+                        _("User Profiles"),
+                        (
+                         ('can_search_users', {'label': _("Can search user profiles")}),
+                         ('can_see_users_emails', {'label': _("Can see members e-mail's")}),
+                         ('can_see_users_trails', {'label': _("Can see members ip's and user-agents")}),
+                         ('can_see_hidden_users', {'label': _("Can see mebers that hide their presence")}),
+                         ),
+                        ))
+
+
+class UsersACL(BaseACL):
+    def can_search_users(self):
+        return self.acl['can_search_users']
+    
+    def can_see_users_emails(self):
+        return self.acl['can_see_users_emails']
+
+    def can_see_users_trails(self):
+        return self.acl['can_see_users_trails']
+
+    def can_see_hidden_users(self):
+        return self.acl['can_see_hidden_users']
+    
+    def allow_details_view(self):
+        if not self.acl['can_see_users_trails']:
+            raise ACLError404()
+
+
+def build(acl, roles):
+    acl.users = UsersACL()
+    acl.users.acl['can_search_users'] = False
+    acl.users.acl['can_see_users_emails'] = False
+    acl.users.acl['can_see_users_trails'] = False
+    acl.users.acl['can_see_hidden_users'] = False
+
+    for role in roles:
+        if 'can_search_users' in role and role['can_search_users']:
+            acl.users.acl['can_search_users'] = True
+
+        if 'can_see_users_emails' in role and role['can_see_users_emails']:
+            acl.users.acl['can_see_users_emails'] = True
+
+        if 'can_see_users_trails' in role and role['can_see_users_trails']:
+            acl.users.acl['can_see_users_trails'] = True
+
+        if 'can_see_hidden_users' in role and role['can_see_hidden_users']:
+            acl.users.acl['can_see_hidden_users'] = True

+ 2 - 1
static/sora/css/sora.css

@@ -1043,7 +1043,8 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .clickable{cursor:pointer;}
 .clickable{cursor:pointer;}
 .profile-header{overflow:auto;border-bottom:none;margin-bottom:30px;}.profile-header .avatar-height{overflow:auto;}.profile-header .avatar-height .avatar{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;margin-right:24px;width:125px;height:125px;}
 .profile-header{overflow:auto;border-bottom:none;margin-bottom:30px;}.profile-header .avatar-height{overflow:auto;}.profile-header .avatar-height .avatar{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;margin-right:24px;width:125px;height:125px;}
 .profile-header .avatar-height h1{font-size:300%;}
 .profile-header .avatar-height h1{font-size:300%;}
-.profile-header .avatar-height .lead{color:#555555;}.profile-header .avatar-height .lead .muted{color:#959595;}
+.profile-header .avatar-height .lead{color:#555555;}.profile-header .avatar-height .lead .badge{position:relative;bottom:3px;}
+.profile-header .avatar-height .lead .muted{color:#959595;}
 .profile-header .tabs-extra{margin-top:-33px;}
 .profile-header .tabs-extra{margin-top:-33px;}
 .profile-header .nav-tabs{margin-top:-22px;margin-bottom:0px;padding-left:142px;}
 .profile-header .nav-tabs{margin-top:-22px;margin-bottom:0px;padding-left:142px;}
 .avatar-menu h3{margin-top:0px;}
 .avatar-menu h3{margin-top:0px;}

+ 6 - 1
static/sora/css/sora/utilities.less

@@ -22,10 +22,15 @@
     h1 {
     h1 {
       font-size: 300%;
       font-size: 300%;
     }
     }
-    
+        
     .lead {
     .lead {
       color: @gray;
       color: @gray;
       
       
+      .badge {
+        position: relative;
+        bottom: 3px;
+      } 
+       
       .muted {
       .muted {
         color: lighten(@gray, 25%);
         color: lighten(@gray, 25%);
       }
       }

+ 82 - 0
templates/sora/profiles/details.html

@@ -20,6 +20,16 @@
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
+        {% if acl.users.can_see_users_emails() %}
+        <tr>
+          <td class="span2">
+             <strong>{% trans %}E-mail Address{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+            <a href="mailto:{{ profile.email }}">{{ profile.email }}</a>
+          </td>
+        </tr>
+        {% endif %}
         <tr>
         <tr>
           <td class="span2">
           <td class="span2">
           	 <strong>{% trans %}Member Since{% endtrans %}</strong>
           	 <strong>{% trans %}Member Since{% endtrans %}</strong>
@@ -33,7 +43,11 @@
           	 <strong>{% trans %}Last Seen{% endtrans %}</strong>
           	 <strong>{% trans %}Last Seen{% endtrans %}</strong>
           </td>
           </td>
           <td class="span4">
           <td class="span4">
+            {% if not hidden or acl.users.can_see_hidden_users() %}
           	{{ profile.last_date|reltimesince }} <span class="muted">{{ profile.last_date|date("DATETIME_FORMAT") }}</span>
           	{{ profile.last_date|reltimesince }} <span class="muted">{{ profile.last_date|date("DATETIME_FORMAT") }}</span>
+          	{% else %}
+          	<em class="muted">{% trans %}Hidden{% endtrans %}</em>
+          	{% endif %}
           </td>
           </td>
         </tr>
         </tr>
       </tbody>
       </tbody>
@@ -144,4 +158,72 @@
     
     
   </div>
   </div>
 </div>
 </div>
+    
+{% if acl.users.can_see_users_trails() %}
+<hr>
+<div class="row">
+  <div class="span6">
+
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th colspan="2">
+            {% trans %}Registration Details{% endtrans %}
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td class="span2">
+             <strong>{% trans %}IP Address{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+            {{ profile.join_ip }}
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+             <strong>{% trans %}UserAgent String{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+            {% if profile.join_agent %}{{ profile.join_agent }}{% else %}<em class="muted">{% trans %}Created from console{% endtrans %}</em>{% endif %}
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    
+  </div>
+  <div class="span6">
+      
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th colspan="2">
+            {% trans %}Last Visit Details{% endtrans %}
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td class="span2">
+             <strong>{% trans %}IP Address{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+            {{ profile.last_ip }}
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+             <strong>{% trans %}UserAgent String{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+            {% if profile.last_agent %}{{ profile.last_agent }}{% else %}<em class="muted">{% trans %}Created from console{% endtrans %}</em>{% endif %}
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    
+  </div>
+</div>
+{% endif %}
 {% endblock %}
 {% endblock %}

+ 2 - 0
templates/sora/profiles/list.html

@@ -17,6 +17,7 @@
   <h1>{% trans %}Users List{% endtrans %} <small>{% trans %}Browse notable user groups or find specific user{% endtrans %}</small></h1>
   <h1>{% trans %}Users List{% endtrans %} <small>{% trans %}Browse notable user groups or find specific user{% endtrans %}</small></h1>
   <ul class="nav nav-tabs">{% for rank in ranks %}
   <ul class="nav nav-tabs">{% for rank in ranks %}
   	<li{% if active_rank.id == rank.id %} class="active"{% endif %}><a href="{% if loop.first %}{% url 'users' %}{% else %}{% url 'users' rank_slug=rank.name_slug %}{% endif %}">{{ _(rank.name) }}</a></li>{% endfor %}
   	<li{% if active_rank.id == rank.id %} class="active"{% endif %}><a href="{% if loop.first %}{% url 'users' %}{% else %}{% url 'users' rank_slug=rank.name_slug %}{% endif %}">{{ _(rank.name) }}</a></li>{% endfor %}
+  	{% if acl.users.can_search_users() and not user.is_crawler() %}
   	<li class="tab-search{% if not ranks %} tab-search-no-tabs{% endif %} pull-right">
   	<li class="tab-search{% if not ranks %} tab-search-no-tabs{% endif %} pull-right">
       <form action="{% url 'users' %}" class="form-inline" method="post">
       <form action="{% url 'users' %}" class="form-inline" method="post">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
         <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
@@ -24,6 +25,7 @@
         <button type="submit" class="btn btn-primary"><i class="icon-search icon-white"></i></button>
         <button type="submit" class="btn btn-primary"><i class="icon-search icon-white"></i></button>
       </form>
       </form>
   	</li>
   	</li>
+  	{% endif %}
   </ul>
   </ul>
 </div>
 </div>
 <h2>{% if in_search %}{% trans %}Search Users{% endtrans %}{% elif active_rank %}{{ _(active_rank.name) }}{% endif %}</h2>
 <h2>{% if in_search %}{% trans %}Search Users{% endtrans %}{% elif active_rank %}{{ _(active_rank.name) }}{% endif %}</h2>

+ 8 - 2
templates/sora/profiles/profile.html

@@ -15,14 +15,20 @@
       <div class="pull-left">
       <div class="pull-left">
         <h1>{{ profile.username }}</h1>
         <h1>{{ profile.username }}</h1>
         <p class="lead">
         <p class="lead">
-          {% if profile.title or profile.rank.title %}{% if profile.title %}{{ _(profile.title) }}{% elif profile.rank.title %}{{ _(profile.rank.title) }}{% endif %}; {% endif %}<span class="muted">{% trans last_visit=profile.last_date|reltimesince|low %}Last seen {{ last_visit }}{% endtrans %}</span>
+          {% if profile.title or profile.rank.title %}{% if profile.title %}{{ _(profile.title) }}{% elif profile.rank.title %}{{ _(profile.rank.title) }}{% endif %};{% endif %}
+          {% if online and (not hidden or acl.users.can_see_hidden_users()) %}<span class="badge badge-success">{% if profile.hide_activity -%}
+              {% trans %}Online, hidden{% endtrans %}
+              {%- else -%}
+              {% trans %}Online{% endtrans %}
+              {%- endif %}</span>{% else %}<span class="badge">{% trans %}Offline{% endtrans %}</span>{% endif %} <span class="muted">{% trans last_visit=profile.last_date|reltimesince|low %}Last seen {{ last_visit }}{% endtrans %}</span>
         </p>
         </p>
       </div>
       </div>
     </div>
     </div>
     <ul class="nav nav-tabs">
     <ul class="nav nav-tabs">
       {% for link in tabs %}
       {% for link in tabs %}
       <li{% if link.active %} class="active"{% endif %}>
       <li{% if link.active %} class="active"{% endif %}>
-        <a href="{{ link.route|url(user=profile.pk, username=profile.username_slug) }}">{{ link.name }}</a></li>
+        <a href="{{ link.route|url(user=profile.pk, username=profile.username_slug) }}">{{ link.name }}</a>
+      </li>
       {% endfor %}
       {% endfor %}
     </ul>
     </ul>
     {% if user.is_authenticated() and user.pk != profile.pk %}
     {% if user.is_authenticated() and user.pk != profile.pk %}

+ 4 - 0
templates/sora/threads/changelog_diff.html

@@ -33,6 +33,10 @@
 <ul class="unstyled thread-info">
 <ul class="unstyled thread-info">
   <li><i class="icon-time"></i> <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
   <li><i class="icon-time"></i> <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
   <li><i class="icon-user"></i> {% if change.user_id %}<a href="{% url 'user' user=change.user_id, username=change.user_slug %}">{{ change.user_name }}</a>{% else %}{{ change.user_name }}{% endif %}</li>
   <li><i class="icon-user"></i> {% if change.user_id %}<a href="{% url 'user' user=change.user_id, username=change.user_slug %}">{{ change.user_name }}</a>{% else %}{{ change.user_name }}{% endif %}</li>
+  {% if acl.users.can_see_users_trails() %}
+  <li><i class="icon-globe"></i> {{ change.ip }}</li>
+  <li><i class="icon-qrcode"></i> {{ change.agent }}</li>
+  {% endif %}
   {% if change.change != 0 %}<li><i class="icon-{% if change.change > 0 %}plus{% elif change.change < 0 %}minus{% endif %}"></i> {% if change.change > 0 -%}
   {% if change.change != 0 %}<li><i class="icon-{% if change.change > 0 %}plus{% elif change.change < 0 %}minus{% endif %}"></i> {% if change.change > 0 -%}
   {% trans chars=change.change %}Added one character{% pluralize %}Added {{ chars }} characters{% endtrans %}
   {% trans chars=change.change %}Added one character{% pluralize %}Added {{ chars }} characters{% endtrans %}
   {%- elif change.change < 0 -%}
   {%- elif change.change < 0 -%}

+ 33 - 0
templates/sora/threads/details.html

@@ -0,0 +1,33 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=(_("Post #%(post)s Info") % {'post': post.pk}),parent=thread.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
+{% for parent in parents %}
+<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+{% endfor %}
+<li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider">/</span></li>
+<li class="active">{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %}
+{%- endblock %}
+
+{% block content %}
+<div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
+  <h1>{% trans post=post.pk %}Post #{{ post }} Info{% endtrans %} <small>{{ thread.name }}</small></h1>
+  <ul class="unstyled thread-info">
+    <li><i class="icon-time"></i> <a href="{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}">{{ post.date|reltimesince }}</a></li>
+    <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+    <li><i class="icon-pencil"></i> {% trans edits=post.edits %}One edit{% pluralize %}{{ edits }} edits{% endtrans %}</li>
+    {% if post.protected %}<li><i class="icon-lock"></i> {% trans %}Protected{% endtrans %}</li>{% endif %}
+  </ul>
+</div>
+<h2>{% trans %}IP Address{% endtrans %}</h2>
+<p class="lead">{{ post.ip }}</p>
+<h2>{% trans %}UserAgent{% endtrans %}</h2>
+<p class="lead">{{ post.agent }}</p>
+{% endblock %}

+ 4 - 1
templates/sora/threads/thread.html

@@ -83,7 +83,7 @@
       {%- endif %}#post-{{ post.pk }}" class="post-perma pull-right tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
       {%- endif %}#post-{{ post.pk }}" class="post-perma pull-right tooltip-left" title="{% trans %}Direct link to this post{% endtrans %}">#{{ pagination['start'] + loop.index }}</a>
     </div>
     </div>
     <div class="post-content">
     <div class="post-content">
-      {% trans user=yabadaba %}This reply was posted by user that is on your ignored list.{% endtrans %}
+      {% trans %}This reply was posted by user that is on your ignored list.{% endtrans %}
     </div>
     </div>
   </div>
   </div>
   {% else %}
   {% else %}
@@ -177,6 +177,9 @@
         {% if not post.deleted and acl.threads.can_delete_post(user, forum, thread, post) -%}
         {% if not post.deleted and acl.threads.can_delete_post(user, forum, thread, post) -%}
             <li><form action="{% url 'post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="prompt-delete-post" method="post"><button type="submit" class="btn danger"><i class="icon-trash"></i> {% trans %}Delete{% endtrans %}</button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>{% endif %}
             <li><form action="{% url 'post_hide' thread=thread.pk, slug=thread.slug, post=post.pk %}" class="prompt-delete-post" method="post"><button type="submit" class="btn danger"><i class="icon-trash"></i> {% trans %}Delete{% endtrans %}</button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>{% endif %}
         {% endif %}
         {% endif %}
+        {% if acl.users.can_see_users_trails() -%}
+        <li><a href="{% url 'post_info' thread=thread.pk, slug=thread.slug, post=post.pk %}"><i class="icon-qrcode"></i> {% trans %}Info{% endtrans %}</a></li>
+        {% endif %}
         {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk -%}
         {% if acl.threads.can_edit_thread(user, forum, thread, post) and thread.start_post_id == post.pk -%}
         <li><a href="{% url 'thread_edit' thread=thread.pk, slug=thread.slug %}"><i class="icon-edit"></i> {% trans %}Edit{% endtrans %}</a></li>
         <li><a href="{% url 'thread_edit' thread=thread.pk, slug=thread.slug %}"><i class="icon-edit"></i> {% trans %}Edit{% endtrans %}</a></li>
         {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}
         {% elif acl.threads.can_edit_reply(user, forum, thread, post) %}