Show More
@@ -457,6 +457,12 b' def make_map(config):' | |||||
457 | action='show_all', conditions=dict(function=check_repo, |
|
457 | action='show_all', conditions=dict(function=check_repo, | |
458 | method=["GET"])) |
|
458 | method=["GET"])) | |
459 |
|
459 | |||
|
460 | rmap.connect('pullrequest_comment', | |||
|
461 | '/{repo_name:.*}/pull-request-comment/{pull_request_id}', | |||
|
462 | controller='pullrequests', | |||
|
463 | action='comment', conditions=dict(function=check_repo, | |||
|
464 | method=["POST"])) | |||
|
465 | ||||
460 | rmap.connect('summary_home', '/{repo_name:.*}/summary', |
|
466 | rmap.connect('summary_home', '/{repo_name:.*}/summary', | |
461 | controller='summary', conditions=dict(function=check_repo)) |
|
467 | controller='summary', conditions=dict(function=check_repo)) | |
462 |
|
468 |
@@ -390,10 +390,10 b' class ChangesetController(BaseRepoContro' | |||||
390 | if status and change_status: |
|
390 | if status and change_status: | |
391 | ChangesetStatusModel().set_status( |
|
391 | ChangesetStatusModel().set_status( | |
392 | c.rhodecode_db_repo.repo_id, |
|
392 | c.rhodecode_db_repo.repo_id, | |
393 | revision, |
|
|||
394 | status, |
|
393 | status, | |
395 | c.rhodecode_user.user_id, |
|
394 | c.rhodecode_user.user_id, | |
396 | comm, |
|
395 | comm, | |
|
396 | revision=revision, | |||
397 | ) |
|
397 | ) | |
398 | action_logger(self.rhodecode_user, |
|
398 | action_logger(self.rhodecode_user, | |
399 | 'user_commented_revision:%s' % revision, |
|
399 | 'user_commented_revision:%s' % revision, |
@@ -24,19 +24,20 b'' | |||||
24 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
24 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
25 | import logging |
|
25 | import logging | |
26 | import traceback |
|
26 | import traceback | |
27 | import binascii |
|
|||
28 |
|
27 | |||
29 | from webob.exc import HTTPNotFound |
|
28 | from webob.exc import HTTPNotFound | |
30 |
|
29 | |||
31 | from pylons import request, response, session, tmpl_context as c, url |
|
30 | from pylons import request, response, session, tmpl_context as c, url | |
32 | from pylons.controllers.util import abort, redirect |
|
31 | from pylons.controllers.util import abort, redirect | |
33 | from pylons.i18n.translation import _ |
|
32 | from pylons.i18n.translation import _ | |
|
33 | from pylons.decorators import jsonify | |||
34 |
|
34 | |||
35 | from rhodecode.lib.base import BaseRepoController, render |
|
35 | from rhodecode.lib.base import BaseRepoController, render | |
36 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
36 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |
37 | from rhodecode.lib import helpers as h |
|
37 | from rhodecode.lib import helpers as h | |
38 | from rhodecode.lib import diffs |
|
38 | from rhodecode.lib import diffs | |
39 | from rhodecode.model.db import User, PullRequest, Repository, ChangesetStatus |
|
39 | from rhodecode.lib.utils import action_logger | |
|
40 | from rhodecode.model.db import User, PullRequest, ChangesetStatus | |||
40 | from rhodecode.model.pull_request import PullRequestModel |
|
41 | from rhodecode.model.pull_request import PullRequestModel | |
41 | from rhodecode.model.meta import Session |
|
42 | from rhodecode.model.meta import Session | |
42 | from rhodecode.model.repo import RepoModel |
|
43 | from rhodecode.model.repo import RepoModel | |
@@ -208,9 +209,56 b' class PullrequestsController(BaseRepoCon' | |||||
208 | .get_comments(c.rhodecode_db_repo.repo_id, |
|
209 | .get_comments(c.rhodecode_db_repo.repo_id, | |
209 | pull_request=pull_request_id) |
|
210 | pull_request=pull_request_id) | |
210 |
|
211 | |||
211 |
# changeset(pull-request) status |
|
212 | # changeset(pull-request) status | |
212 | c.current_changeset_status = ChangesetStatusModel()\ |
|
213 | c.current_changeset_status = ChangesetStatusModel()\ | |
213 |
.get_status(c. |
|
214 | .get_status(c.pull_request.org_repo, | |
214 |
pull_request=pull_request |
|
215 | pull_request=c.pull_request) | |
215 | c.changeset_statuses = ChangesetStatus.STATUSES |
|
216 | c.changeset_statuses = ChangesetStatus.STATUSES | |
216 | return render('/pullrequests/pullrequest_show.html') |
|
217 | return render('/pullrequests/pullrequest_show.html') | |
|
218 | ||||
|
219 | @jsonify | |||
|
220 | def comment(self, repo_name, pull_request_id): | |||
|
221 | ||||
|
222 | status = request.POST.get('changeset_status') | |||
|
223 | change_status = request.POST.get('change_changeset_status') | |||
|
224 | ||||
|
225 | comm = ChangesetCommentsModel().create( | |||
|
226 | text=request.POST.get('text'), | |||
|
227 | repo_id=c.rhodecode_db_repo.repo_id, | |||
|
228 | user_id=c.rhodecode_user.user_id, | |||
|
229 | pull_request=pull_request_id, | |||
|
230 | f_path=request.POST.get('f_path'), | |||
|
231 | line_no=request.POST.get('line'), | |||
|
232 | status_change=(ChangesetStatus.get_status_lbl(status) | |||
|
233 | if status and change_status else None) | |||
|
234 | ) | |||
|
235 | ||||
|
236 | # get status if set ! | |||
|
237 | if status and change_status: | |||
|
238 | ChangesetStatusModel().set_status( | |||
|
239 | c.rhodecode_db_repo.repo_id, | |||
|
240 | status, | |||
|
241 | c.rhodecode_user.user_id, | |||
|
242 | comm, | |||
|
243 | pull_request=pull_request_id | |||
|
244 | ) | |||
|
245 | action_logger(self.rhodecode_user, | |||
|
246 | 'user_commented_pull_request:%s' % pull_request_id, | |||
|
247 | c.rhodecode_db_repo, self.ip_addr, self.sa) | |||
|
248 | ||||
|
249 | Session.commit() | |||
|
250 | ||||
|
251 | if not request.environ.get('HTTP_X_PARTIAL_XHR'): | |||
|
252 | return redirect(h.url('pullrequest_show', repo_name=repo_name, | |||
|
253 | pull_request_id=pull_request_id)) | |||
|
254 | ||||
|
255 | data = { | |||
|
256 | 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), | |||
|
257 | } | |||
|
258 | if comm: | |||
|
259 | c.co = comm | |||
|
260 | data.update(comm.get_dict()) | |||
|
261 | data.update({'rendered_text': | |||
|
262 | render('changeset/changeset_comment_block.html')}) | |||
|
263 | ||||
|
264 | return data No newline at end of file |
@@ -66,12 +66,15 b' class ChangesetStatusModel(BaseModel):' | |||||
66 | else: |
|
66 | else: | |
67 | raise Exception('Please specify revision or pull_request') |
|
67 | raise Exception('Please specify revision or pull_request') | |
68 |
|
68 | |||
69 | status = q.scalar() |
|
69 | # need to use first here since there can be multiple statuses | |
|
70 | # returned from pull_request | |||
|
71 | status = q.first() | |||
70 | status = status.status if status else status |
|
72 | status = status.status if status else status | |
71 | st = status or ChangesetStatus.DEFAULT |
|
73 | st = status or ChangesetStatus.DEFAULT | |
72 | return str(st) |
|
74 | return str(st) | |
73 |
|
75 | |||
74 |
def set_status(self, repo, |
|
76 | def set_status(self, repo, status, user, comment, revision=None, | |
|
77 | pull_request=None): | |||
75 | """ |
|
78 | """ | |
76 | Creates new status for changeset or updates the old ones bumping their |
|
79 | Creates new status for changeset or updates the old ones bumping their | |
77 | version, leaving the current status at |
|
80 | version, leaving the current status at | |
@@ -89,20 +92,48 b' class ChangesetStatusModel(BaseModel):' | |||||
89 | """ |
|
92 | """ | |
90 | repo = self._get_repo(repo) |
|
93 | repo = self._get_repo(repo) | |
91 |
|
94 | |||
92 |
|
|
95 | q = ChangesetStatus.query() | |
93 | .filter(ChangesetStatus.repo == repo)\ |
|
96 | ||
94 | .filter(ChangesetStatus.revision == revision)\ |
|
97 | if revision: | |
95 | .all() |
|
98 | q = q.filter(ChangesetStatus.repo == repo) | |
|
99 | q = q.filter(ChangesetStatus.revision == revision) | |||
|
100 | elif pull_request: | |||
|
101 | pull_request = self.__get_pull_request(pull_request) | |||
|
102 | q = q.filter(ChangesetStatus.repo == pull_request.org_repo) | |||
|
103 | q = q.filter(ChangesetStatus.pull_request == pull_request) | |||
|
104 | cur_statuses = q.all() | |||
|
105 | ||||
96 | if cur_statuses: |
|
106 | if cur_statuses: | |
97 | for st in cur_statuses: |
|
107 | for st in cur_statuses: | |
98 | st.version += 1 |
|
108 | st.version += 1 | |
99 | self.sa.add(st) |
|
109 | self.sa.add(st) | |
100 | new_status = ChangesetStatus() |
|
110 | ||
101 | new_status.author = self._get_user(user) |
|
111 | def _create_status(user, repo, status, comment, revision, pull_request): | |
102 | new_status.repo = self._get_repo(repo) |
|
112 | new_status = ChangesetStatus() | |
103 |
new_status. |
|
113 | new_status.author = self._get_user(user) | |
104 |
new_status.re |
|
114 | new_status.repo = self._get_repo(repo) | |
105 |
new_status. |
|
115 | new_status.status = status | |
106 | self.sa.add(new_status) |
|
116 | new_status.comment = comment | |
107 | return new_status |
|
117 | new_status.revision = revision | |
|
118 | new_status.pull_request = pull_request | |||
|
119 | return new_status | |||
108 |
|
120 | |||
|
121 | if revision: | |||
|
122 | new_status = _create_status(user=user, repo=repo, status=status, | |||
|
123 | comment=comment, revision=revision, | |||
|
124 | pull_request=None) | |||
|
125 | self.sa.add(new_status) | |||
|
126 | return new_status | |||
|
127 | elif pull_request: | |||
|
128 | #pull request can have more than one revision associated to it | |||
|
129 | #we need to create new version for each one | |||
|
130 | new_statuses = [] | |||
|
131 | repo = pull_request.org_repo | |||
|
132 | for rev in pull_request.revisions: | |||
|
133 | new_status = _create_status(user=user, repo=repo, | |||
|
134 | status=status, comment=comment, | |||
|
135 | revision=rev, | |||
|
136 | pull_request=pull_request) | |||
|
137 | new_statuses.append(new_status) | |||
|
138 | self.sa.add(new_status) | |||
|
139 | return new_statuses |
@@ -55,38 +55,54 b' class ChangesetCommentsModel(BaseModel):' | |||||
55 | user_objects.append(user_obj) |
|
55 | user_objects.append(user_obj) | |
56 | return user_objects |
|
56 | return user_objects | |
57 |
|
57 | |||
58 |
def create(self, text, repo_id, user_id, revision, |
|
58 | def create(self, text, repo_id, user_id, revision=None, pull_request=None, | |
59 | line_no=None, status_change=None): |
|
59 | f_path=None, line_no=None, status_change=None): | |
60 | """ |
|
60 | """ | |
61 |
Creates new comment for changeset |
|
61 | Creates new comment for changeset or pull request. | |
62 |
this comment is associated with a |
|
62 | IF status_change is not none this comment is associated with a | |
|
63 | status change of changeset or changesets associated with pull request | |||
63 |
|
64 | |||
64 | :param text: |
|
65 | :param text: | |
65 | :param repo_id: |
|
66 | :param repo_id: | |
66 | :param user_id: |
|
67 | :param user_id: | |
67 | :param revision: |
|
68 | :param revision: | |
|
69 | :param pull_request: | |||
68 | :param f_path: |
|
70 | :param f_path: | |
69 | :param line_no: |
|
71 | :param line_no: | |
70 | :param status_change: |
|
72 | :param status_change: | |
71 | """ |
|
73 | """ | |
|
74 | if not text: | |||
|
75 | return | |||
72 |
|
76 | |||
73 | if text: |
|
77 | repo = Repository.get(repo_id) | |
74 | repo = Repository.get(repo_id) |
|
78 | comment = ChangesetComment() | |
|
79 | comment.repo = repo | |||
|
80 | comment.user_id = user_id | |||
|
81 | comment.text = text | |||
|
82 | comment.f_path = f_path | |||
|
83 | comment.line_no = line_no | |||
|
84 | ||||
|
85 | if revision: | |||
75 | cs = repo.scm_instance.get_changeset(revision) |
|
86 | cs = repo.scm_instance.get_changeset(revision) | |
76 | desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256)) |
|
87 | desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256)) | |
77 | author_email = cs.author_email |
|
88 | author_email = cs.author_email | |
78 | comment = ChangesetComment() |
|
|||
79 | comment.repo = repo |
|
|||
80 | comment.user_id = user_id |
|
|||
81 | comment.revision = revision |
|
89 | comment.revision = revision | |
82 | comment.text = text |
|
90 | elif pull_request: | |
83 | comment.f_path = f_path |
|
91 | pull_request = self.__get_pull_request(pull_request) | |
84 |
comment. |
|
92 | comment.pull_request = pull_request | |
|
93 | desc = '' | |||
|
94 | else: | |||
|
95 | raise Exception('Please specify revision or pull_request_id') | |||
85 |
|
96 | |||
86 |
|
|
97 | self.sa.add(comment) | |
87 |
|
|
98 | self.sa.flush() | |
88 | # make notification |
|
99 | ||
89 | line = '' |
|
100 | # make notification | |
|
101 | line = '' | |||
|
102 | body = text | |||
|
103 | ||||
|
104 | #changeset | |||
|
105 | if revision: | |||
90 | if line_no: |
|
106 | if line_no: | |
91 | line = _('on line %s') % line_no |
|
107 | line = _('on line %s') % line_no | |
92 | subj = safe_unicode( |
|
108 | subj = safe_unicode( | |
@@ -99,34 +115,41 b' class ChangesetCommentsModel(BaseModel):' | |||||
99 | ) |
|
115 | ) | |
100 | ) |
|
116 | ) | |
101 | ) |
|
117 | ) | |
102 |
|
118 | notification_type = Notification.TYPE_CHANGESET_COMMENT | ||
103 | body = text |
|
|||
104 |
|
||||
105 | # get the current participants of this changeset |
|
119 | # get the current participants of this changeset | |
106 | recipients = ChangesetComment.get_users(revision=revision) |
|
120 | recipients = ChangesetComment.get_users(revision=revision) | |
107 |
|
||||
108 | # add changeset author if it's in rhodecode system |
|
121 | # add changeset author if it's in rhodecode system | |
109 | recipients += [User.get_by_email(author_email)] |
|
122 | recipients += [User.get_by_email(author_email)] | |
|
123 | #pull request | |||
|
124 | elif pull_request: | |||
|
125 | #TODO: make this something usefull | |||
|
126 | subj = 'commented on pull request something...' | |||
|
127 | notification_type = Notification.TYPE_PULL_REQUEST_COMMENT | |||
|
128 | # get the current participants of this pull request | |||
|
129 | recipients = ChangesetComment.get_users(pull_request_id= | |||
|
130 | pull_request.pull_request_id) | |||
|
131 | # add pull request author | |||
|
132 | recipients += [pull_request.author] | |||
110 |
|
133 | |||
111 |
|
|
134 | # create notification objects, and emails | |
|
135 | NotificationModel().create( | |||
|
136 | created_by=user_id, subject=subj, body=body, | |||
|
137 | recipients=recipients, type_=notification_type, | |||
|
138 | email_kwargs={'status_change': status_change} | |||
|
139 | ) | |||
|
140 | ||||
|
141 | mention_recipients = set(self._extract_mentions(body))\ | |||
|
142 | .difference(recipients) | |||
|
143 | if mention_recipients: | |||
|
144 | subj = _('[Mention]') + ' ' + subj | |||
112 | NotificationModel().create( |
|
145 | NotificationModel().create( | |
113 | created_by=user_id, subject=subj, body=body, |
|
146 | created_by=user_id, subject=subj, body=body, | |
114 | recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT, |
|
147 | recipients=mention_recipients, | |
115 | email_kwargs={'status_change': status_change} |
|
148 | type_=notification_type, | |
|
149 | email_kwargs={'status_change': status_change} | |||
116 | ) |
|
150 | ) | |
117 |
|
151 | |||
118 | mention_recipients = set(self._extract_mentions(body))\ |
|
152 | return comment | |
119 | .difference(recipients) |
|
|||
120 | if mention_recipients: |
|
|||
121 | subj = _('[Mention]') + ' ' + subj |
|
|||
122 | NotificationModel().create( |
|
|||
123 | created_by=user_id, subject=subj, body=body, |
|
|||
124 | recipients=mention_recipients, |
|
|||
125 | type_=Notification.TYPE_CHANGESET_COMMENT, |
|
|||
126 | email_kwargs={'status_change': status_change} |
|
|||
127 | ) |
|
|||
128 |
|
||||
129 | return comment |
|
|||
130 |
|
153 | |||
131 | def delete(self, comment): |
|
154 | def delete(self, comment): | |
132 | """ |
|
155 | """ |
@@ -766,7 +766,12 b' class Repository(Base, BaseModel):' | |||||
766 | statuses = statuses.filter(ChangesetStatus.revision.in_(revisions)) |
|
766 | statuses = statuses.filter(ChangesetStatus.revision.in_(revisions)) | |
767 | grouped = {} |
|
767 | grouped = {} | |
768 | for stat in statuses.all(): |
|
768 | for stat in statuses.all(): | |
769 | grouped[stat.revision] = [str(stat.status), stat.status_lbl] |
|
769 | pr_id = pr_repo = None | |
|
770 | if stat.pull_request: | |||
|
771 | pr_id = stat.pull_request.pull_request_id | |||
|
772 | pr_repo = stat.pull_request.other_repo.repo_name | |||
|
773 | grouped[stat.revision] = [str(stat.status), stat.status_lbl, | |||
|
774 | pr_id, pr_repo] | |||
770 | return grouped |
|
775 | return grouped | |
771 |
|
776 | |||
772 | #========================================================================== |
|
777 | #========================================================================== | |
@@ -1336,17 +1341,21 b' class ChangesetComment(Base, BaseModel):' | |||||
1336 | pull_request = relationship('PullRequest', lazy='joined') |
|
1341 | pull_request = relationship('PullRequest', lazy='joined') | |
1337 |
|
1342 | |||
1338 | @classmethod |
|
1343 | @classmethod | |
1339 | def get_users(cls, revision): |
|
1344 | def get_users(cls, revision=None, pull_request_id=None): | |
1340 | """ |
|
1345 | """ | |
1341 |
Returns user associated with this |
|
1346 | Returns user associated with this ChangesetComment. ie those | |
1342 | who actually commented |
|
1347 | who actually commented | |
1343 |
|
1348 | |||
1344 | :param cls: |
|
1349 | :param cls: | |
1345 | :param revision: |
|
1350 | :param revision: | |
1346 | """ |
|
1351 | """ | |
1347 |
|
|
1352 | q = Session.query(User)\ | |
1348 | .filter(cls.revision == revision)\ |
|
1353 | .join(ChangesetComment.author) | |
1349 | .join(ChangesetComment.author).all() |
|
1354 | if revision: | |
|
1355 | q = q.filter(cls.revision == revision) | |||
|
1356 | elif pull_request_id: | |||
|
1357 | q = q.filter(cls.pull_request_id == pull_request_id) | |||
|
1358 | return q.all() | |||
1350 |
|
1359 | |||
1351 |
|
1360 | |||
1352 | class ChangesetStatus(Base, BaseModel): |
|
1361 | class ChangesetStatus(Base, BaseModel): | |
@@ -1457,6 +1466,7 b' class Notification(Base, BaseModel):' | |||||
1457 | TYPE_MENTION = u'mention' |
|
1466 | TYPE_MENTION = u'mention' | |
1458 | TYPE_REGISTRATION = u'registration' |
|
1467 | TYPE_REGISTRATION = u'registration' | |
1459 | TYPE_PULL_REQUEST = u'pull_request' |
|
1468 | TYPE_PULL_REQUEST = u'pull_request' | |
|
1469 | TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment' | |||
1460 |
|
1470 | |||
1461 | notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) |
|
1471 | notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) | |
1462 | subject = Column('subject', Unicode(512), nullable=True) |
|
1472 | subject = Column('subject', Unicode(512), nullable=True) |
@@ -198,13 +198,15 b' class NotificationModel(BaseModel):' | |||||
198 | Creates a human readable description based on properties |
|
198 | Creates a human readable description based on properties | |
199 | of notification object |
|
199 | of notification object | |
200 | """ |
|
200 | """ | |
201 |
|
201 | #alias | ||
|
202 | _n = notification | |||
202 | _map = { |
|
203 | _map = { | |
203 |
|
|
204 | _n.TYPE_CHANGESET_COMMENT: _('commented on commit'), | |
204 |
|
|
205 | _n.TYPE_MESSAGE: _('sent message'), | |
205 |
|
|
206 | _n.TYPE_MENTION: _('mentioned you'), | |
206 |
|
|
207 | _n.TYPE_REGISTRATION: _('registered in RhodeCode'), | |
207 |
|
|
208 | _n.TYPE_PULL_REQUEST: _('opened new pull request'), | |
|
209 | _n.TYPE_PULL_REQUEST_COMMENT: _('commented on pull request') | |||
208 | } |
|
210 | } | |
209 |
|
211 | |||
210 | tmpl = "%(user)s %(action)s %(when)s" |
|
212 | tmpl = "%(user)s %(action)s %(when)s" |
@@ -85,7 +85,13 b'' | |||||
85 | <div class="changeset-status-container"> |
|
85 | <div class="changeset-status-container"> | |
86 | %if c.statuses.get(cs.raw_id): |
|
86 | %if c.statuses.get(cs.raw_id): | |
87 | <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div> |
|
87 | <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div> | |
88 |
<div class="changeset-status-ico"> |
|
88 | <div class="changeset-status-ico"> | |
|
89 | %if c.statuses.get(cs.raw_id)[2]: | |||
|
90 | <a class="tooltip" title="${_('Click to open associated pull request')}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></a> | |||
|
91 | %else: | |||
|
92 | <img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /> | |||
|
93 | %endif | |||
|
94 | </div> | |||
89 | %endif |
|
95 | %endif | |
90 | </div> |
|
96 | </div> | |
91 | </div> |
|
97 | </div> |
@@ -70,8 +70,8 b'' | |||||
70 | ##${comment.comment_inline_form(c.changeset)} |
|
70 | ##${comment.comment_inline_form(c.changeset)} | |
71 |
|
71 | |||
72 | ## render comments main comments form and it status |
|
72 | ## render comments main comments form and it status | |
73 |
|
|
73 | ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), | |
74 |
|
|
74 | c.current_changeset_status)} | |
75 |
|
75 | |||
76 | </div> |
|
76 | </div> | |
77 |
|
77 |
General Comments 0
You need to be logged in to leave comments.
Login now