# HG changeset patch # User Marcin Kuzminski # Date 2013-01-21 00:03:44 # Node ID cd50d1b5f35bf73310fbb358d3754a2c35b9da6b # Parent a0737406ce26ec2687a3b603f87ff0729d1e8f08 # Parent a64e663abda36b3d354b541b95b45330382bd2c7 merged with beta diff --git a/CONTRIBUTORS b/CONTRIBUTORS --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -32,3 +32,4 @@ List of contributors to RhodeCode projec Raoul Thill Philip Jameson Mads Kiilerich + Dan Sheridan diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -155,9 +155,10 @@ OUTPUT:: lock ---- -Set locking state on given repository by given user. +Set locking state on given repository by given user. If userid param is skipped +, then it is set to id of user whos calling this method. This command can be executed only using api_key belonging to user with admin -rights. +rights or regular user that have admin or write access to repository. INPUT:: @@ -166,9 +167,8 @@ INPUT:: method : "lock" args : { "repoid" : "" - "userid" : "", + "userid" : "", "locked" : "" - } OUTPUT:: @@ -178,12 +178,47 @@ OUTPUT:: error : null +show_ip +------- + +Shows IP address as seen from RhodeCode server, together with all +defined IP addresses for given user. +This command can be executed only using api_key belonging to user with admin +rights. + +INPUT:: + + id : + api_key : "" + method : "show_ip" + args : { + "userid" : "", + } + +OUTPUT:: + + id : + result : { + "ip_addr_server": ", + "user_ips": [ + { + "ip_addr": "", + "ip_range": ["", ""], + }, + ... + ] + } + + error : null + + get_user -------- Get's an user by username or user_id, Returns empty result if user is not found. +If userid param is skipped it is set to id of user who is calling this method. This command can be executed only using api_key belonging to user with admin -rights. +rights, or regular users that cannot specify different userid than theirs INPUT:: @@ -192,7 +227,7 @@ INPUT:: api_key : "" method : "get_user" args : { - "userid" : "" + "userid" : "" } OUTPUT:: @@ -200,16 +235,17 @@ OUTPUT:: id : result: None if user does not exist or { - "user_id" : "", - "username" : "", - "firstname": "", - "lastname" : "", - "email" : "", - "emails": "", - "active" : "", - "admin" :  "", - "ldap_dn" : "", - "last_login": "", + "user_id" : "", + "username" : "", + "firstname": "", + "lastname" : "", + "email" : "", + "emails": "", + "ip_addresses": "", + "active" : "", + "admin" :  "", + "ldap_dn" : "", + "last_login": "", "permissions": { "global": ["hg.create.repository", "repository.read", @@ -241,16 +277,17 @@ OUTPUT:: id : result: [ { - "user_id" : "", - "username" : "", - "firstname": "", - "lastname" : "", - "email" : "", - "emails": "", - "active" : "", - "admin" :  "", - "ldap_dn" : "", - "last_login": "", + "user_id" : "", + "username" : "", + "firstname": "", + "lastname" : "", + "email" : "", + "emails": "", + "ip_addresses": "", + "active" : "", + "admin" :  "", + "ldap_dn" : "", + "last_login": "", }, … ] @@ -315,14 +352,14 @@ INPUT:: method : "update_user" args : { "userid" : "", - "username" : " = Optional", - "email" : " = Optional", - "password" : " = Optional", - "firstname" : " = Optional", - "lastname" : " = Optional", - "active" : " = Optional", - "admin" : " = Optional", - "ldap_dn" : " = Optional" + "username" : " = Optional(None)", + "email" : " = Optional(None)", + "password" : " = Optional(None)", + "firstname" : " = Optional(None)", + "lastname" : " = Optional(None)", + "active" : " = Optional(None)", + "admin" : " = Optional(None)", + "ldap_dn" : " = Optional(None)" } OUTPUT:: @@ -537,8 +574,9 @@ get_repo -------- Gets an existing repository by it's name or repository_id. Members will return -either users_group or user associated to that repository. This command can -be executed only using api_key belonging to user with admin rights. +either users_group or user associated to that repository. This command can be +executed only using api_key belonging to user with admin +rights or regular user that have at least read access to repository. INPUT:: @@ -555,29 +593,40 @@ OUTPUT:: id : result: None if repository does not exist or { - "repo_id" : "", - "repo_name" : "" - "repo_type" : "", - "clone_uri" : "", - "private": : "", - "created_on" : "", - "description" : "", - "landing_rev": "", - "owner": "", - "fork_of": "", + "repo_id" : "", + "repo_name" : "" + "repo_type" : "", + "clone_uri" : "", + "enable_downloads": "", + "enable_locking": "", + "enable_statistics": "", + "private": "", + "created_on" : "", + "description" : "", + "landing_rev": "", + "last_changeset": { + "author": "", + "date": "", + "message": "", + "raw_id": "", + "revision": "", + "short_id": "" + } + "owner": "", + "fork_of": "", "members" : [ { "type": "user", - "user_id" : "", - "username" : "", - "firstname": "", - "lastname" : "", - "email" : "", - "emails": "", - "active" : "", - "admin" :  "", - "ldap_dn" : "", - "last_login": "", + "user_id" : "", + "username" : "", + "firstname": "", + "lastname" : "", + "email" : "", + "emails": "", + "active" : "", + "admin" :  "", + "ldap_dn" : "", + "last_login": "", "permission" : "repository.(read|write|admin)" }, … @@ -597,8 +646,9 @@ OUTPUT:: get_repos --------- -Lists all existing repositories. This command can be executed only using api_key -belonging to user with admin rights +Lists all existing repositories. This command can be executed only using +api_key belonging to user with admin rights or regular user that have +admin, write or read access to repository. INPUT:: @@ -613,16 +663,19 @@ OUTPUT:: id : result: [ { - "repo_id" : "", - "repo_name" : "" - "repo_type" : "", - "clone_uri" : "", - "private": : "", - "created_on" : "", - "description" : "", - "landing_rev": "", - "owner": "", - "fork_of": "", + "repo_id" : "", + "repo_name" : "" + "repo_type" : "", + "clone_uri" : "", + "private": : "", + "created_on" : "", + "description" : "", + "landing_rev": "", + "owner": "", + "fork_of": "", + "enable_downloads": "", + "enable_locking": "", + "enable_statistics": "", }, … ] @@ -666,11 +719,12 @@ OUTPUT:: create_repo ----------- -Creates a repository. This command can be executed only using api_key -belonging to user with admin rights. -If repository name contains "/", all needed repository groups will be created. -For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent), -and create "baz" repository with "bar" as group. +Creates a repository. If repository name contains "/", all needed repository +groups will be created. For example "foo/bar/baz" will create groups +"foo", "bar" (with "foo" as parent), and create "baz" repository with +"bar" as group. This command can be executed only using api_key belonging to user with admin +rights or regular user that have create repository permission. Regular users +cannot specify owner parameter INPUT:: @@ -679,13 +733,16 @@ INPUT:: api_key : "" method : "create_repo" args: { - "repo_name" : "", - "owner" : "", - "repo_type" : "", - "description" : " = Optional('')", - "private" : " = Optional(False)", - "clone_uri" : " = Optional(None)", - "landing_rev" : " = Optional('tip')", + "repo_name" : "", + "owner" : "", + "repo_type" : " = Optional('hg')", + "description" : " = Optional('')", + "private" : " = Optional(False)", + "clone_uri" : " = Optional(None)", + "landing_rev" : " = Optional('tip')", + "enable_downloads": " = Optional(False)", + "enable_locking": " = Optional(False)", + "enable_statistics": " = Optional(False)", } OUTPUT:: @@ -694,26 +751,65 @@ OUTPUT:: result: { "msg": "Created new repository ``", "repo": { - "repo_id" : "", - "repo_name" : "" - "repo_type" : "", - "clone_uri" : "", - "private": : "", - "created_on" : "", - "description" : "", - "landing_rev": "", - "owner": "", - "fork_of": "", + "repo_id" : "", + "repo_name" : "" + "repo_type" : "", + "clone_uri" : "", + "private": : "", + "created_on" : "", + "description" : "", + "landing_rev": "", + "owner": "", + "fork_of": "", + "enable_downloads": "", + "enable_locking": "", + "enable_statistics": "", }, } error: null +fork_repo +--------- + +Creates a fork of given repo. In case of using celery this will +immidiatelly return success message, while fork is going to be created +asynchronous. This command can be executed only using api_key belonging to +user with admin rights or regular user that have fork permission, and at least +read access to forking repository. Regular users cannot specify owner parameter. + + +INPUT:: + + id : + api_key : "" + method : "fork_repo" + args: { + "repoid" : "", + "fork_name": "", + "owner": "", + "description": "", + "copy_permissions": "", + "private": "", + "landing_rev": "" + + } + +OUTPUT:: + + id : + result: { + "msg": "Created fork of `` as ``", + "success": true + } + error: null + + delete_repo ----------- -Deletes a repository. This command can be executed only using api_key -belonging to user with admin rights. +Deletes a repository. This command can be executed only using api_key belonging to user with admin +rights or regular user that have admin access to repository. INPUT:: diff --git a/docs/changelog.rst b/docs/changelog.rst --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,38 @@ Changelog ========= +1.5.2 (**2013-01-14**) +---------------------- + +news +++++ + +- IP restrictions for users. Each user can get a set of whitelist IP+mask for + extra protection. Useful for buildbots etc. +- added full last changeset info to lightweight dashboard. lightweight dashboard + is now fully functional replacement of original dashboard. +- implemented certain API calls for non-admin users. +- enabled all Markdown Extra plugins +- implemented #725 Pull Request View - Show origin repo URL +- show comments from pull requests into associated changesets + +fixes ++++++ + +- update repoinfo script is more failsafe +- fixed #687 Lazy loaded tooltip bug with simultaneous ajax requests +- fixed #691: Notifications for pull requests: move link to top for better + readability +- fixed #699: fix missing fork docs for API +- fixed #693 Opening changeset from pull request fails +- fixed #710 File view stripping empty lines from beginning and end of file +- fixed issues with getting repos by path on windows, caused GIT hooks to fail +- fixed issues with groups paginator on main dashboard +- improved fetch/pull command for git repos, now pulling all refs +- fixed issue #719 Journal revision ID tooltip AJAX query path is incorrect + when running in a subdir +- fixed issue #702 API methods without arguments fail when "args":null +- set the status of changesets initially on pull request. Fixes issues #690 and #587 1.5.1 (**2012-12-13**) ---------------------- diff --git a/docs/installation.rst b/docs/installation.rst --- a/docs/installation.rst +++ b/docs/installation.rst @@ -43,6 +43,10 @@ For installing RhodeCode i highly recomm way many required by RhodeCode libraries will remain sandboxed from your main python and making things less problematic when doing system python updates. +Alternative very detailed installation instructions for Ubuntu Server with +celery, indexer and daemon scripts: https://gist.github.com/4546398 + + - Assuming you have installed virtualenv_ create a new virtual environment using virtualenv command:: diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -26,7 +26,7 @@ import sys import platform -VERSION = (1, 5, 1) +VERSION = (1, 5, 2) try: from rhodecode.lib import get_current_revision @@ -38,7 +38,7 @@ except ImportError: __version__ = ('.'.join((str(each) for each in VERSION[:3])) + '.'.join(VERSION[3:])) -__dbversion__ = 9 # defines current db version for migrations +__dbversion__ = 10 # defines current db version for migrations __platform__ = platform.system() __license__ = 'GPLv3' __py_version__ = sys.version_info diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -222,6 +222,10 @@ def make_map(config): action="add_email", conditions=dict(method=["PUT"])) m.connect("user_emails_delete", "/users_emails/{id}", action="delete_email", conditions=dict(method=["DELETE"])) + m.connect("user_ips", "/users_ips/{id}", + action="add_ip", conditions=dict(method=["PUT"])) + m.connect("user_ips_delete", "/users_ips/{id}", + action="delete_ip", conditions=dict(method=["DELETE"])) #ADMIN USERS GROUPS REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, @@ -355,8 +359,6 @@ def make_map(config): m.connect('api', '/api') #USER JOURNAL - rmap.connect('journal_my_repos', '%s/journal_my_repos' % ADMIN_PREFIX, - controller='journal', action='index_my_repos') rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal', action='index') rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX, diff --git a/rhodecode/controllers/admin/notifications.py b/rhodecode/controllers/admin/notifications.py --- a/rhodecode/controllers/admin/notifications.py +++ b/rhodecode/controllers/admin/notifications.py @@ -110,8 +110,8 @@ class NotificationsController(BaseContro # url('notification', notification_id=ID) try: no = Notification.get(notification_id) - owner = lambda: (no.notifications_to_users.user.user_id - == c.rhodecode_user.user_id) + owner = all(un.user.user_id == c.rhodecode_user.user_id + for un in no.notifications_to_users) if h.HasPermissionAny('hg.admin')() or owner: NotificationModel().mark_read(c.rhodecode_user.user_id, no) Session().commit() @@ -132,8 +132,8 @@ class NotificationsController(BaseContro try: no = Notification.get(notification_id) - owner = lambda: (no.notifications_to_users.user.user_id - == c.rhodecode_user.user_id) + owner = all(un.user.user_id == c.rhodecode_user.user_id + for un in no.notifications_to_users) if h.HasPermissionAny('hg.admin')() or owner: NotificationModel().delete(c.rhodecode_user.user_id, no) Session().commit() @@ -149,8 +149,8 @@ class NotificationsController(BaseContro c.user = self.rhodecode_user no = Notification.get(notification_id) - owner = lambda: (no.notifications_to_users.user.user_id - == c.user.user_id) + owner = all(un.user.user_id == c.rhodecode_user.user_id + for un in no.notifications_to_users) if no and (h.HasPermissionAny('hg.admin', 'repository.admin')() or owner): unotification = NotificationModel()\ .get_user_notification(c.user.user_id, no) diff --git a/rhodecode/controllers/admin/permissions.py b/rhodecode/controllers/admin/permissions.py --- a/rhodecode/controllers/admin/permissions.py +++ b/rhodecode/controllers/admin/permissions.py @@ -33,11 +33,12 @@ from pylons.controllers.util import abor from pylons.i18n.translation import _ from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator,\ + AuthUser from rhodecode.lib.base import BaseController, render from rhodecode.model.forms import DefaultPermissionsForm from rhodecode.model.permission import PermissionModel -from rhodecode.model.db import User +from rhodecode.model.db import User, UserIpMap from rhodecode.model.meta import Session log = logging.getLogger(__name__) @@ -105,36 +106,41 @@ class PermissionsController(BaseControll # h.form(url('permission', id=ID), # method='put') # url('permission', id=ID) - - permission_model = PermissionModel() + if id == 'default': + c.user = default_user = User.get_by_username('default') + c.perm_user = AuthUser(user_id=default_user.user_id) + c.user_ip_map = UserIpMap.query()\ + .filter(UserIpMap.user == default_user).all() + permission_model = PermissionModel() - _form = DefaultPermissionsForm([x[0] for x in self.repo_perms_choices], - [x[0] for x in self.group_perms_choices], - [x[0] for x in self.register_choices], - [x[0] for x in self.create_choices], - [x[0] for x in self.fork_choices])() + _form = DefaultPermissionsForm( + [x[0] for x in self.repo_perms_choices], + [x[0] for x in self.group_perms_choices], + [x[0] for x in self.register_choices], + [x[0] for x in self.create_choices], + [x[0] for x in self.fork_choices])() - try: - form_result = _form.to_python(dict(request.POST)) - form_result.update({'perm_user_name': id}) - permission_model.update(form_result) - Session().commit() - h.flash(_('Default permissions updated successfully'), - category='success') + try: + form_result = _form.to_python(dict(request.POST)) + form_result.update({'perm_user_name': id}) + permission_model.update(form_result) + Session().commit() + h.flash(_('Default permissions updated successfully'), + category='success') - except formencode.Invalid, errors: - defaults = errors.value + except formencode.Invalid, errors: + defaults = errors.value - return htmlfill.render( - render('admin/permissions/permissions.html'), - defaults=defaults, - errors=errors.error_dict or {}, - prefix_error=False, - encoding="UTF-8") - except Exception: - log.error(traceback.format_exc()) - h.flash(_('error occurred during update of permissions'), - category='error') + return htmlfill.render( + render('admin/permissions/permissions.html'), + defaults=defaults, + errors=errors.error_dict or {}, + prefix_error=False, + encoding="UTF-8") + except Exception: + log.error(traceback.format_exc()) + h.flash(_('error occurred during update of permissions'), + category='error') return redirect(url('edit_permission', id=id)) @@ -157,10 +163,11 @@ class PermissionsController(BaseControll #this form can only edit default user permissions if id == 'default': - default_user = User.get_by_username('default') - defaults = {'_method': 'put', - 'anonymous': default_user.active} - + c.user = default_user = User.get_by_username('default') + defaults = {'anonymous': default_user.active} + c.perm_user = AuthUser(user_id=default_user.user_id) + c.user_ip_map = UserIpMap.query()\ + .filter(UserIpMap.user == default_user).all() for p in default_user.user_perms: if p.permission.permission_name.startswith('repository.'): defaults['default_repo_perm'] = p.permission.permission_name @@ -181,7 +188,7 @@ class PermissionsController(BaseControll render('admin/permissions/permissions.html'), defaults=defaults, encoding="UTF-8", - force_defaults=True, + force_defaults=False ) else: return redirect(url('admin_home')) diff --git a/rhodecode/controllers/admin/repos.py b/rhodecode/controllers/admin/repos.py --- a/rhodecode/controllers/admin/repos.py +++ b/rhodecode/controllers/admin/repos.py @@ -135,40 +135,10 @@ class ReposController(BaseController): .order_by(func.lower(Repository.repo_name))\ .all() - repos_data = [] - total_records = len(c.repos_list) - - _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup - template = _tmpl_lookup.get_template('data_table/_dt_elements.html') - - quick_menu = lambda repo_name: (template.get_def("quick_menu") - .render(repo_name, _=_, h=h, c=c)) - repo_lnk = lambda name, rtype, private, fork_of: ( - template.get_def("repo_name") - .render(name, rtype, private, fork_of, short_name=False, - admin=True, _=_, h=h, c=c)) - - repo_actions = lambda repo_name: (template.get_def("repo_actions") - .render(repo_name, _=_, h=h, c=c)) - - for repo in c.repos_list: - repos_data.append({ - "menu": quick_menu(repo.repo_name), - "raw_name": repo.repo_name.lower(), - "name": repo_lnk(repo.repo_name, repo.repo_type, - repo.private, repo.fork), - "desc": repo.description, - "owner": repo.user.username, - "action": repo_actions(repo.repo_name), - }) - - c.data = json.dumps({ - "totalRecords": total_records, - "startIndex": 0, - "sort": "name", - "dir": "asc", - "records": repos_data - }) + repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list, + admin=True) + #json used to render the grid + c.data = json.dumps(repos_data) return render('admin/repos/repos.html') diff --git a/rhodecode/controllers/admin/repos_groups.py b/rhodecode/controllers/admin/repos_groups.py --- a/rhodecode/controllers/admin/repos_groups.py +++ b/rhodecode/controllers/admin/repos_groups.py @@ -295,54 +295,18 @@ class ReposGroupsController(BaseControll c.groups = self.scm_model.get_repos_groups(groups) if c.visual.lightweight_dashboard is False: - c.cached_repo_list = self.scm_model.get_repos(all_repos=gr_filter) - - c.repos_list = c.cached_repo_list + c.repos_list = self.scm_model.get_repos(all_repos=gr_filter) ## lightweight version of dashboard else: c.repos_list = Repository.query()\ .filter(Repository.group_id == id)\ .order_by(func.lower(Repository.repo_name))\ .all() - repos_data = [] - total_records = len(c.repos_list) - _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup - template = _tmpl_lookup.get_template('data_table/_dt_elements.html') - - quick_menu = lambda repo_name: (template.get_def("quick_menu") - .render(repo_name, _=_, h=h, c=c)) - repo_lnk = lambda name, rtype, private, fork_of: ( - template.get_def("repo_name") - .render(name, rtype, private, fork_of, short_name=False, - admin=False, _=_, h=h, c=c)) - last_change = lambda last_change: (template.get_def("last_change") - .render(last_change, _=_, h=h, c=c)) - rss_lnk = lambda repo_name: (template.get_def("rss") - .render(repo_name, _=_, h=h, c=c)) - atom_lnk = lambda repo_name: (template.get_def("atom") - .render(repo_name, _=_, h=h, c=c)) - - for repo in c.repos_list: - repos_data.append({ - "menu": quick_menu(repo.repo_name), - "raw_name": repo.repo_name.lower(), - "name": repo_lnk(repo.repo_name, repo.repo_type, - repo.private, repo.fork), - "last_change": last_change(repo.last_db_change), - "desc": repo.description, - "owner": h.person(repo.user.username), - "rss": rss_lnk(repo.repo_name), - "atom": atom_lnk(repo.repo_name), - }) - - c.data = json.dumps({ - "totalRecords": total_records, - "startIndex": 0, - "sort": "name", - "dir": "asc", - "records": repos_data - }) + repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list, + admin=False) + #json used to render the grid + c.data = json.dumps(repos_data) return render('admin/repos_groups/repos_groups.html') diff --git a/rhodecode/controllers/admin/settings.py b/rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py +++ b/rhodecode/controllers/admin/settings.py @@ -48,11 +48,12 @@ from rhodecode.model.forms import UserFo ApplicationUiSettingsForm, ApplicationVisualisationForm from rhodecode.model.scm import ScmModel from rhodecode.model.user import UserModel +from rhodecode.model.repo import RepoModel from rhodecode.model.db import User from rhodecode.model.notification import EmailNotificationModel from rhodecode.model.meta import Session -from rhodecode.lib.utils2 import str2bool - +from rhodecode.lib.utils2 import str2bool, safe_unicode +from rhodecode.lib.compat import json log = logging.getLogger(__name__) @@ -119,10 +120,11 @@ class SettingsController(BaseController) invalidate_cache('get_repo_cached_%s' % repo_name) added, removed = repo2db_mapper(initial, rm_obsolete) - - h.flash(_('Repositories successfully' - ' rescanned added: %s,removed: %s') % (added, removed), - category='success') + _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-' + h.flash(_('Repositories successfully ' + 'rescanned added: %s ; removed: %s') % + (_repr(added), _repr(removed)), + category='success') if setting_id == 'whoosh': repo_location = self._get_hg_ui_settings()['paths_root_path'] @@ -336,7 +338,7 @@ class SettingsController(BaseController) .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT, body=test_email_body) - recipients = [test_email] if [test_email] else None + recipients = [test_email] if test_email else None run_task(tasks.send_email, recipients, test_email_subj, test_email_body, test_email_html_body) @@ -381,6 +383,17 @@ class SettingsController(BaseController) force_defaults=False ) + def _load_my_repos_data(self): + repos_list = Session().query(Repository)\ + .filter(Repository.user_id == + self.rhodecode_user.user_id)\ + .order_by(func.lower(Repository.repo_name)).all() + + repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list, + admin=True) + #json used to render the grid + return json.dumps(repos_data) + @NotAnonymous() def my_account(self): """ @@ -389,17 +402,16 @@ class SettingsController(BaseController) # url('admin_settings_my_account') c.user = User.get(self.rhodecode_user.user_id) - all_repos = Session().query(Repository)\ - .filter(Repository.user_id == c.user.user_id)\ - .order_by(func.lower(Repository.repo_name)).all() - - c.user_repos = ScmModel().get_repos(all_repos) + c.ldap_dn = c.user.ldap_dn if c.user.username == 'default': h.flash(_("You can't edit this user since it's" " crucial for entire application"), category='warning') return redirect(url('users')) + #json used to render the grid + c.data = self._load_my_repos_data() + defaults = c.user.get_dict() c.form = htmlfill.render( @@ -420,19 +432,25 @@ class SettingsController(BaseController) # method='put') # url('admin_settings_my_account_update', id=ID) uid = self.rhodecode_user.user_id + c.user = User.get(self.rhodecode_user.user_id) + c.ldap_dn = c.user.ldap_dn email = self.rhodecode_user.email _form = UserForm(edit=True, old_data={'user_id': uid, 'email': email})() form_result = {} try: form_result = _form.to_python(dict(request.POST)) - UserModel().update_my_account(uid, form_result) + skip_attrs = ['admin', 'active'] # skip attr for my account + if c.ldap_dn: + #forbid updating username for ldap accounts + skip_attrs.append('username') + UserModel().update(uid, form_result, skip_attrs=skip_attrs) h.flash(_('Your account was updated successfully'), category='success') Session().commit() except formencode.Invalid, errors: - c.user = User.get(self.rhodecode_user.user_id) - + #json used to render the grid + c.data = self._load_my_repos_data() c.form = htmlfill.render( render('admin/users/user_edit_my_account_form.html'), defaults=errors.value, @@ -448,23 +466,14 @@ class SettingsController(BaseController) return redirect(url('my_account')) @NotAnonymous() - def my_account_my_repos(self): - all_repos = Session().query(Repository)\ - .filter(Repository.user_id == self.rhodecode_user.user_id)\ - .order_by(func.lower(Repository.repo_name))\ - .all() - c.user_repos = ScmModel().get_repos(all_repos) - return render('admin/users/user_edit_my_account_repos.html') - - @NotAnonymous() def my_account_my_pullrequests(self): c.my_pull_requests = PullRequest.query()\ - .filter(PullRequest.user_id== + .filter(PullRequest.user_id == self.rhodecode_user.user_id)\ .all() c.participate_in_pull_requests = \ [x.pull_request for x in PullRequestReviewers.query()\ - .filter(PullRequestReviewers.user_id== + .filter(PullRequestReviewers.user_id == self.rhodecode_user.user_id)\ .all()] return render('admin/users/user_edit_my_account_pullrequests.html') diff --git a/rhodecode/controllers/admin/users.py b/rhodecode/controllers/admin/users.py --- a/rhodecode/controllers/admin/users.py +++ b/rhodecode/controllers/admin/users.py @@ -41,7 +41,7 @@ from rhodecode.lib.auth import LoginRequ AuthUser from rhodecode.lib.base import BaseController, render -from rhodecode.model.db import User, UserEmailMap +from rhodecode.model.db import User, UserEmailMap, UserIpMap from rhodecode.model.forms import UserForm from rhodecode.model.user import UserModel from rhodecode.model.meta import Session @@ -159,7 +159,7 @@ class UsersController(BaseController): user_model = UserModel() c.user = user_model.get(id) c.ldap_dn = c.user.ldap_dn - c.perm_user = AuthUser(user_id=id) + c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr) _form = UserForm(edit=True, old_data={'user_id': id, 'email': c.user.email})() form_result = {} @@ -178,6 +178,8 @@ class UsersController(BaseController): except formencode.Invalid, errors: c.user_email_map = UserEmailMap.query()\ .filter(UserEmailMap.user == c.user).all() + c.user_ip_map = UserIpMap.query()\ + .filter(UserIpMap.user == c.user).all() defaults = errors.value e = errors.error_dict or {} defaults.update({ @@ -231,12 +233,14 @@ class UsersController(BaseController): h.flash(_("You can't edit this user"), category='warning') return redirect(url('users')) - c.perm_user = AuthUser(user_id=id) + c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr) c.user.permissions = {} c.granted_permissions = UserModel().fill_perms(c.user)\ .permissions['global'] c.user_email_map = UserEmailMap.query()\ .filter(UserEmailMap.user == c.user).all() + c.user_ip_map = UserIpMap.query()\ + .filter(UserIpMap.user == c.user).all() user_model = UserModel() c.ldap_dn = c.user.ldap_dn defaults = c.user.get_dict() @@ -299,7 +303,6 @@ class UsersController(BaseController): """POST /user_emails:Add an existing item""" # url('user_emails', id=ID, method='put') - #TODO: validation and form !!! email = request.POST.get('new_email') user_model = UserModel() @@ -324,3 +327,36 @@ class UsersController(BaseController): Session().commit() h.flash(_("Removed email from user"), category='success') return redirect(url('edit_user', id=id)) + + def add_ip(self, id): + """POST /user_ips:Add an existing item""" + # url('user_ips', id=ID, method='put') + + ip = request.POST.get('new_ip') + user_model = UserModel() + + try: + user_model.add_extra_ip(id, ip) + Session().commit() + h.flash(_("Added ip %s to user") % ip, category='success') + except formencode.Invalid, error: + msg = error.error_dict['ip'] + h.flash(msg, category='error') + except Exception: + log.error(traceback.format_exc()) + h.flash(_('An error occurred during ip saving'), + category='error') + if 'default_user' in request.POST: + return redirect(url('edit_permission', id='default')) + return redirect(url('edit_user', id=id)) + + def delete_ip(self, id): + """DELETE /user_ips_delete/id: Delete an existing item""" + # url('user_ips_delete', id=ID, method='delete') + user_model = UserModel() + user_model.delete_extra_ip(id, request.POST.get('del_ip')) + Session().commit() + h.flash(_("Removed ip from user"), category='success') + if 'default_user' in request.POST: + return redirect(url('edit_permission', id='default')) + return redirect(url('edit_user', id=id)) diff --git a/rhodecode/controllers/api/__init__.py b/rhodecode/controllers/api/__init__.py --- a/rhodecode/controllers/api/__init__.py +++ b/rhodecode/controllers/api/__init__.py @@ -32,17 +32,15 @@ import urllib import traceback import time -from rhodecode.lib.compat import izip_longest, json - from paste.response import replace_header - from pylons.controllers import WSGIController - from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \ HTTPBadRequest, HTTPError from rhodecode.model.db import User +from rhodecode.model import meta +from rhodecode.lib.compat import izip_longest, json from rhodecode.lib.auth import AuthUser from rhodecode.lib.base import _get_ip_addr, _get_access_path from rhodecode.lib.utils2 import safe_unicode @@ -86,6 +84,9 @@ class JSONRPCController(WSGIController): """ + def _get_ip_addr(self, environ): + return _get_ip_addr(environ) + def _get_method_args(self): """ Return `self._rpc_args` to dispatched controller method @@ -99,6 +100,7 @@ class JSONRPCController(WSGIController): controller and if it exists, dispatch to it. """ start = time.time() + ip_addr = self.ip_addr = self._get_ip_addr(environ) self._req_id = None if 'CONTENT_LENGTH' not in environ: log.debug("No Content-Length") @@ -130,6 +132,9 @@ class JSONRPCController(WSGIController): self._req_id = json_body['id'] self._req_method = json_body['method'] self._request_params = json_body['args'] + if not isinstance(self._request_params, dict): + self._request_params = {} + log.debug( 'method: %s, params: %s' % (self._req_method, self._request_params) @@ -144,7 +149,15 @@ class JSONRPCController(WSGIController): if u is None: return jsonrpc_error(retid=self._req_id, message='Invalid API KEY') - auth_u = AuthUser(u.user_id, self._req_api_key) + + #check if we are allowed to use this IP + auth_u = AuthUser(u.user_id, self._req_api_key, ip_addr=ip_addr) + if not auth_u.ip_allowed: + return jsonrpc_error(retid=self._req_id, + message='request from IP:%s not allowed' % (ip_addr)) + else: + log.info('Access for IP:%s allowed' % (ip_addr)) + except Exception, e: return jsonrpc_error(retid=self._req_id, message='Invalid API KEY') @@ -202,6 +215,7 @@ class JSONRPCController(WSGIController): ) self._rpc_args = {USER_SESSION_ATTR: u} + self._rpc_args.update(self._request_params) self._rpc_args['action'] = self._req_method diff --git a/rhodecode/controllers/api/api.py b/rhodecode/controllers/api/api.py --- a/rhodecode/controllers/api/api.py +++ b/rhodecode/controllers/api/api.py @@ -27,10 +27,12 @@ import traceback import logging +from pylons.controllers.util import abort from rhodecode.controllers.api import JSONRPCController, JSONRPCError -from rhodecode.lib.auth import HasPermissionAllDecorator, \ - HasPermissionAnyDecorator, PasswordGenerator, AuthUser +from rhodecode.lib.auth import PasswordGenerator, AuthUser, \ + HasPermissionAllDecorator, HasPermissionAnyDecorator, \ + HasPermissionAnyApi, HasRepoPermissionAnyApi from rhodecode.lib.utils import map_groups, repo2db_mapper from rhodecode.model.meta import Session from rhodecode.model.scm import ScmModel @@ -38,11 +40,27 @@ from rhodecode.model.repo import RepoMod from rhodecode.model.user import UserModel from rhodecode.model.users_group import UsersGroupModel from rhodecode.model.permission import PermissionModel -from rhodecode.model.db import Repository +from rhodecode.model.db import Repository, RhodeCodeSetting, UserIpMap log = logging.getLogger(__name__) +class OptionalAttr(object): + """ + Special Optional Option that defines other attribute + """ + def __init__(self, attr_name): + self.attr_name = attr_name + + def __repr__(self): + return '' % self.attr_name + + def __call__(self): + return self +#alias +OAttr = OptionalAttr + + class Optional(object): """ Defines an optional parameter:: @@ -184,10 +202,11 @@ class ApiController(JSONRPCController): 'Error occurred during rescan repositories action' ) - @HasPermissionAllDecorator('hg.admin') - def lock(self, apiuser, repoid, userid, locked): + def lock(self, apiuser, repoid, locked, userid=Optional(OAttr('apiuser'))): """ - Set locking state on particular repository by given user + Set locking state on particular repository by given user, if + this command is runned by non-admin account userid is set to user + who is calling this method :param apiuser: :param repoid: @@ -195,6 +214,22 @@ class ApiController(JSONRPCController): :param locked: """ repo = get_repo_or_error(repoid) + if HasPermissionAnyApi('hg.admin')(user=apiuser): + pass + elif HasRepoPermissionAnyApi('repository.admin', + 'repository.write')(user=apiuser, + repo_name=repo.repo_name): + #make sure normal user does not pass someone else userid, + #he is not allowed to do that + if not isinstance(userid, Optional) and userid != apiuser.user_id: + raise JSONRPCError( + 'userid is not the same as your user' + ) + else: + raise JSONRPCError('repository `%s` does not exist' % (repoid)) + + if isinstance(userid, Optional): + userid = apiuser.user_id user = get_user_or_error(userid) locked = bool(locked) try: @@ -212,13 +247,38 @@ class ApiController(JSONRPCController): ) @HasPermissionAllDecorator('hg.admin') - def get_user(self, apiuser, userid): - """" - Get a user by username + def show_ip(self, apiuser, userid): + """ + Shows IP address as seen from RhodeCode server, together with all + defined IP addresses for given user :param apiuser: :param userid: """ + user = get_user_or_error(userid) + ips = UserIpMap.query().filter(UserIpMap.user == user).all() + return dict( + ip_addr_server=self.ip_addr, + user_ips=ips + ) + + def get_user(self, apiuser, userid=Optional(OAttr('apiuser'))): + """" + Get a user by username, or userid, if userid is given + + :param apiuser: + :param userid: + """ + if HasPermissionAnyApi('hg.admin')(user=apiuser) is False: + #make sure normal user does not pass someone else userid, + #he is not allowed to do that + if not isinstance(userid, Optional) and userid != apiuser.user_id: + raise JSONRPCError( + 'userid is not the same as your user' + ) + + if isinstance(userid, Optional): + userid = apiuser.user_id user = get_user_or_error(userid) data = user.get_api_data() @@ -479,7 +539,6 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') def get_repo(self, apiuser, repoid): """" Get repository by name @@ -489,6 +548,12 @@ class ApiController(JSONRPCController): """ repo = get_repo_or_error(repoid) + if HasPermissionAnyApi('hg.admin')(user=apiuser) is False: + # check if we have admin permission for this repo ! + if HasRepoPermissionAnyApi('repository.admin')(user=apiuser, + repo_name=repo.repo_name) is False: + raise JSONRPCError('repository `%s` does not exist' % (repoid)) + members = [] for user in repo.repo_to_perm: perm = user.permission.permission_name @@ -510,20 +575,23 @@ class ApiController(JSONRPCController): data['members'] = members return data - @HasPermissionAnyDecorator('hg.admin') def get_repos(self, apiuser): """" Get all repositories :param apiuser: """ + result = [] + if HasPermissionAnyApi('hg.admin')(user=apiuser) is False: + repos = RepoModel().get_all_user_repos(user=apiuser) + else: + repos = RepoModel().get_all() - result = [] - for repo in RepoModel().get_all(): + for repo in repos: result.append(repo.get_api_data()) return result - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def get_repo_nodes(self, apiuser, repoid, revision, root_path, ret_type='all'): """ @@ -556,12 +624,16 @@ class ApiController(JSONRPCController): ) @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository') - def create_repo(self, apiuser, repo_name, owner, repo_type, + def create_repo(self, apiuser, repo_name, owner=Optional(OAttr('apiuser')), + repo_type=Optional('hg'), description=Optional(''), private=Optional(False), - clone_uri=Optional(None), landing_rev=Optional('tip')): + clone_uri=Optional(None), landing_rev=Optional('tip'), + enable_statistics=Optional(False), + enable_locking=Optional(False), + enable_downloads=Optional(False)): """ Create repository, if clone_url is given it makes a remote clone - if repo_name is withina group name the groups will be created + if repo_name is within a group name the groups will be created automatically if they aren't present :param apiuser: @@ -573,12 +645,32 @@ class ApiController(JSONRPCController): :param clone_uri: :param landing_rev: """ + if HasPermissionAnyApi('hg.admin')(user=apiuser) is False: + if not isinstance(owner, Optional): + #forbid setting owner for non-admins + raise JSONRPCError( + 'Only RhodeCode admin can specify `owner` param' + ) + if isinstance(owner, Optional): + owner = apiuser.user_id + owner = get_user_or_error(owner) if RepoModel().get_by_repo_name(repo_name): raise JSONRPCError("repo `%s` already exist" % repo_name) - private = Optional.extract(private) + defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True) + if isinstance(private, Optional): + private = defs.get('repo_private') or Optional.extract(private) + if isinstance(repo_type, Optional): + repo_type = defs.get('repo_type') + if isinstance(enable_statistics, Optional): + enable_statistics = defs.get('repo_enable_statistics') + if isinstance(enable_locking, Optional): + enable_locking = defs.get('repo_enable_locking') + if isinstance(enable_downloads, Optional): + enable_downloads = defs.get('repo_enable_downloads') + clone_uri = Optional.extract(clone_uri) description = Optional.extract(description) landing_rev = Optional.extract(landing_rev) @@ -596,32 +688,51 @@ class ApiController(JSONRPCController): clone_uri=clone_uri, repos_group=group, landing_rev=landing_rev, + enable_statistics=enable_statistics, + enable_downloads=enable_downloads, + enable_locking=enable_locking ) Session().commit() - return dict( msg="Created new repository `%s`" % (repo.repo_name), repo=repo.get_api_data() ) - except Exception: log.error(traceback.format_exc()) raise JSONRPCError('failed to create repository `%s`' % repo_name) - @HasPermissionAnyDecorator('hg.admin') - def fork_repo(self, apiuser, repoid, fork_name, owner, + @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository') + def fork_repo(self, apiuser, repoid, fork_name, owner=Optional(OAttr('apiuser')), description=Optional(''), copy_permissions=Optional(False), private=Optional(False), landing_rev=Optional('tip')): repo = get_repo_or_error(repoid) repo_name = repo.repo_name - owner = get_user_or_error(owner) _repo = RepoModel().get_by_repo_name(fork_name) if _repo: type_ = 'fork' if _repo.fork else 'repo' raise JSONRPCError("%s `%s` already exist" % (type_, fork_name)) + if HasPermissionAnyApi('hg.admin')(user=apiuser): + pass + elif HasRepoPermissionAnyApi('repository.admin', + 'repository.write', + 'repository.read')(user=apiuser, + repo_name=repo.repo_name): + if not isinstance(owner, Optional): + #forbid setting owner for non-admins + raise JSONRPCError( + 'Only RhodeCode admin can specify `owner` param' + ) + else: + raise JSONRPCError('repository `%s` does not exist' % (repoid)) + + if isinstance(owner, Optional): + owner = apiuser.user_id + + owner = get_user_or_error(owner) + try: # create structure of groups and return the last group group = map_groups(fork_name) @@ -652,7 +763,6 @@ class ApiController(JSONRPCController): fork_name) ) - @HasPermissionAnyDecorator('hg.admin') def delete_repo(self, apiuser, repoid): """ Deletes a given repository @@ -662,6 +772,12 @@ class ApiController(JSONRPCController): """ repo = get_repo_or_error(repoid) + if HasPermissionAnyApi('hg.admin')(user=apiuser) is False: + # check if we have admin permission for this repo ! + if HasRepoPermissionAnyApi('repository.admin')(user=apiuser, + repo_name=repo.repo_name) is False: + raise JSONRPCError('repository `%s` does not exist' % (repoid)) + try: RepoModel().delete(repo) Session().commit() @@ -675,7 +791,7 @@ class ApiController(JSONRPCController): 'failed to delete repository `%s`' % repo.repo_name ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def grant_user_permission(self, apiuser, repoid, userid, perm): """ Grant permission for user on given repository, or update existing one @@ -708,7 +824,7 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def revoke_user_permission(self, apiuser, repoid, userid): """ Revoke permission for user on given repository @@ -739,7 +855,7 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def grant_users_group_permission(self, apiuser, repoid, usersgroupid, perm): """ @@ -778,7 +894,7 @@ class ApiController(JSONRPCController): ) ) - @HasPermissionAnyDecorator('hg.admin') + @HasPermissionAllDecorator('hg.admin') def revoke_users_group_permission(self, apiuser, repoid, usersgroupid): """ Revoke permission for users group on given repository diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -221,13 +221,25 @@ class ChangesetController(BaseRepoContro for changeset in c.cs_ranges: inlines = [] if method == 'show': - c.statuses.extend([ChangesetStatusModel()\ - .get_status(c.rhodecode_db_repo.repo_id, - changeset.raw_id)]) + c.statuses.extend([ChangesetStatusModel().get_status( + c.rhodecode_db_repo.repo_id, changeset.raw_id)]) c.comments.extend(ChangesetCommentsModel()\ .get_comments(c.rhodecode_db_repo.repo_id, revision=changeset.raw_id)) + + #comments from PR + st = ChangesetStatusModel().get_statuses( + c.rhodecode_db_repo.repo_id, changeset.raw_id, + with_revisions=True) + # from associated statuses, check the pull requests, and + # show comments from them + + prs = set([x.pull_request for x in + filter(lambda x: x.pull_request != None, st)]) + + for pr in prs: + c.comments.extend(pr.comments) inlines = ChangesetCommentsModel()\ .get_inline_comments(c.rhodecode_db_repo.repo_id, revision=changeset.raw_id) @@ -269,6 +281,9 @@ class ChangesetController(BaseRepoContro cs_changes[''] = [None, None, None, None, diff, None] c.changes[changeset.raw_id] = cs_changes + #sort comments by how they were generated + c.comments = sorted(c.comments, key=lambda x: x.comment_id) + # count inline comments for __, lines in c.inline_comments: for comments in lines.values(): @@ -342,7 +357,7 @@ class ChangesetController(BaseRepoContro ) except StatusChangeOnClosedPullRequestError: log.error(traceback.format_exc()) - msg = _('Changing status on a changeset associated with' + msg = _('Changing status on a changeset associated with ' 'a closed pull request is not allowed') h.flash(msg, category='warning') return redirect(h.url('changeset_home', repo_name=repo_name, @@ -371,7 +386,7 @@ class ChangesetController(BaseRepoContro @jsonify def delete_comment(self, repo_name, comment_id): co = ChangesetComment.get(comment_id) - owner = lambda: co.author.user_id == c.rhodecode_user.user_id + owner = co.author.user_id == c.rhodecode_user.user_id if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner: ChangesetCommentsModel().delete(comment=co) Session().commit() diff --git a/rhodecode/controllers/compare.py b/rhodecode/controllers/compare.py --- a/rhodecode/controllers/compare.py +++ b/rhodecode/controllers/compare.py @@ -103,8 +103,11 @@ class CompareController(BaseRepoControll c.org_repo = org_repo = Repository.get_by_repo_name(org_repo) c.other_repo = other_repo = Repository.get_by_repo_name(other_repo) - if c.org_repo is None or c.other_repo is None: - log.error('Could not found repo %s or %s' % (org_repo, other_repo)) + if c.org_repo is None: + log.error('Could not find org repo %s' % org_repo) + raise HTTPNotFound + if c.other_repo is None: + log.error('Could not find other repo %s' % other_repo) raise HTTPNotFound if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo): diff --git a/rhodecode/controllers/home.py b/rhodecode/controllers/home.py --- a/rhodecode/controllers/home.py +++ b/rhodecode/controllers/home.py @@ -28,6 +28,7 @@ import logging from pylons import tmpl_context as c, request from pylons.i18n.translation import _ from webob.exc import HTTPBadRequest +from sqlalchemy.sql.expression import func import rhodecode from rhodecode.lib import helpers as h @@ -35,7 +36,8 @@ from rhodecode.lib.ext_json import json from rhodecode.lib.auth import LoginRequired from rhodecode.lib.base import BaseController, render from rhodecode.model.db import Repository -from sqlalchemy.sql.expression import func +from rhodecode.model.repo import RepoModel + log = logging.getLogger(__name__) @@ -58,51 +60,11 @@ class HomeController(BaseController): .filter(Repository.group_id == None)\ .order_by(func.lower(Repository.repo_name))\ .all() - repos_data = [] - total_records = len(c.repos_list) - _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup - template = _tmpl_lookup.get_template('data_table/_dt_elements.html') - - quick_menu = lambda repo_name: (template.get_def("quick_menu") - .render(repo_name, _=_, h=h, c=c)) - repo_lnk = lambda name, rtype, private, fork_of: ( - template.get_def("repo_name") - .render(name, rtype, private, fork_of, short_name=False, - admin=False, _=_, h=h, c=c)) - last_change = lambda last_change: (template.get_def("last_change") - .render(last_change, _=_, h=h, c=c)) - rss_lnk = lambda repo_name: (template.get_def("rss") - .render(repo_name, _=_, h=h, c=c)) - atom_lnk = lambda repo_name: (template.get_def("atom") - .render(repo_name, _=_, h=h, c=c)) - - def desc(desc): - if c.visual.stylify_metatags: - return h.urlify_text(h.desc_stylize(h.truncate(desc, 60))) - else: - return h.urlify_text(h.truncate(desc, 60)) - - for repo in c.repos_list: - repos_data.append({ - "menu": quick_menu(repo.repo_name), - "raw_name": repo.repo_name.lower(), - "name": repo_lnk(repo.repo_name, repo.repo_type, - repo.private, repo.fork), - "last_change": last_change(repo.last_db_change), - "desc": desc(repo.description), - "owner": h.person(repo.user.username), - "rss": rss_lnk(repo.repo_name), - "atom": atom_lnk(repo.repo_name), - }) - - c.data = json.dumps({ - "totalRecords": total_records, - "startIndex": 0, - "sort": "name", - "dir": "asc", - "records": repos_data - }) + repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list, + admin=False) + #json used to render the grid + c.data = json.dumps(repos_data) return render('/index.html') diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py --- a/rhodecode/controllers/journal.py +++ b/rhodecode/controllers/journal.py @@ -27,6 +27,8 @@ from itertools import groupby from sqlalchemy import or_ from sqlalchemy.orm import joinedload +from sqlalchemy.sql.expression import func + from webhelpers.paginate import Page from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed @@ -39,10 +41,10 @@ from rhodecode.lib.auth import LoginRequ from rhodecode.lib.base import BaseController, render from rhodecode.model.db import UserLog, UserFollowing, Repository, User from rhodecode.model.meta import Session -from sqlalchemy.sql.expression import func -from rhodecode.model.scm import ScmModel from rhodecode.lib.utils2 import safe_int, AttributeDict from rhodecode.controllers.admin.admin import _journal_filter +from rhodecode.model.repo import RepoModel +from rhodecode.lib.compat import json log = logging.getLogger(__name__) @@ -78,18 +80,73 @@ class JournalController(BaseController): c.journal_data = render('journal/journal_data.html') if request.environ.get('HTTP_X_PARTIAL_XHR'): return c.journal_data - return render('journal/journal.html') + + repos_list = Session().query(Repository)\ + .filter(Repository.user_id == + self.rhodecode_user.user_id)\ + .order_by(func.lower(Repository.repo_name)).all() + + repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list, + admin=True) + #json used to render the grid + c.data = json.dumps(repos_data) + + watched_repos_data = [] + + ## watched repos + _render = RepoModel._render_datatable + + def quick_menu(repo_name): + return _render('quick_menu', repo_name) + + def repo_lnk(name, rtype, private, fork_of): + return _render('repo_name', name, rtype, private, fork_of, + short_name=False, admin=False) + + def last_rev(repo_name, cs_cache): + return _render('revision', repo_name, cs_cache.get('revision'), + cs_cache.get('raw_id'), cs_cache.get('author'), + cs_cache.get('message')) - @LoginRequired() - @NotAnonymous() - def index_my_repos(self): - c.user = User.get(self.rhodecode_user.user_id) - if request.environ.get('HTTP_X_PARTIAL_XHR'): - all_repos = self.sa.query(Repository)\ - .filter(Repository.user_id == c.user.user_id)\ - .order_by(func.lower(Repository.repo_name)).all() - c.user_repos = ScmModel().get_repos(all_repos) - return render('journal/journal_page_repos.html') + def desc(desc): + from pylons import tmpl_context as c + if c.visual.stylify_metatags: + return h.urlify_text(h.desc_stylize(h.truncate(desc, 60))) + else: + return h.urlify_text(h.truncate(desc, 60)) + + def repo_actions(repo_name): + return _render('repo_actions', repo_name) + + def owner_actions(user_id, username): + return _render('user_name', user_id, username) + + def toogle_follow(repo_id): + return _render('toggle_follow', repo_id) + + for entry in c.following: + repo = entry.follows_repository + cs_cache = repo.changeset_cache + row = { + "menu": quick_menu(repo.repo_name), + "raw_name": repo.repo_name.lower(), + "name": repo_lnk(repo.repo_name, repo.repo_type, + repo.private, repo.fork), + "last_changeset": last_rev(repo.repo_name, cs_cache), + "raw_tip": cs_cache.get('revision'), + "action": toogle_follow(repo.repo_id) + } + + watched_repos_data.append(row) + + c.watched_data = json.dumps({ + "totalRecords": len(c.following), + "startIndex": 0, + "sort": "name", + "dir": "asc", + "records": watched_repos_data + }) + return render('journal/journal.html') @LoginRequired(api_access=True) @NotAnonymous() diff --git a/rhodecode/controllers/login.py b/rhodecode/controllers/login.py --- a/rhodecode/controllers/login.py +++ b/rhodecode/controllers/login.py @@ -54,10 +54,9 @@ class LoginController(BaseController): def index(self): # redirect if already logged in c.came_from = request.GET.get('came_from') - - if self.rhodecode_user.is_authenticated \ - and self.rhodecode_user.username != 'default': - + not_default = self.rhodecode_user.username != 'default' + ip_allowed = self.rhodecode_user.ip_allowed + if self.rhodecode_user.is_authenticated and not_default and ip_allowed: return redirect(url('home')) if request.POST: diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py --- a/rhodecode/controllers/pullrequests.py +++ b/rhodecode/controllers/pullrequests.py @@ -97,7 +97,7 @@ class PullrequestsController(BaseRepoCon return repo.branches.keys()[0] def _get_is_allowed_change_status(self, pull_request): - owner = self.rhodecode_user.user_id == pull_request.user_id + owner = self.rhodecode_user.user_id == pull_request.user_id reviewer = self.rhodecode_user.user_id in [x.user_id for x in pull_request.reviewers] return (self.rhodecode_user.admin or owner or reviewer) @@ -299,7 +299,7 @@ class PullrequestsController(BaseRepoCon else EmptyChangeset(), 'raw_id')) c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges]) - c.target_repo = c.repo_name + c.target_repo = other_repo.repo_name # defines that we need hidden inputs with changesets c.as_form = request.GET.get('as_form', False) @@ -339,7 +339,6 @@ class PullrequestsController(BaseRepoCon c.users_array = repo_model.get_users_js() c.users_groups_array = repo_model.get_users_groups_js() c.pull_request = PullRequest.get_or_404(pull_request_id) - c.target_repo = c.pull_request.org_repo.repo_name c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request) cc_model = ChangesetCommentsModel() cs_model = ChangesetStatusModel() @@ -478,7 +477,7 @@ class PullrequestsController(BaseRepoCon #don't allow deleting comments on closed pull request raise HTTPForbidden() - owner = lambda: co.author.user_id == c.rhodecode_user.user_id + owner = co.author.user_id == c.rhodecode_user.user_id if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner: ChangesetCommentsModel().delete(comment=co) Session().commit() diff --git a/rhodecode/i18n/ja/LC_MESSAGES/rhodecode.mo b/rhodecode/i18n/ja/LC_MESSAGES/rhodecode.mo index 2f94cd23b5009fe843b7a38ebd7d54300142115d..5372cec08566c5f7fd9f9f696e577084a6f48332 GIT binary patch literal 65250 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\n" "Language-Team: ja \n" "Plural-Forms: nplurals=1; plural=0\n" @@ -41,9 +41,9 @@ msgstr "" #: rhodecode/controllers/changeset.py:314 #: rhodecode/controllers/pullrequests.py:417 -#, fuzzy, python-format +#, python-format msgid "Status change -> %s" -msgstr "" +msgstr "ステータス変更 -> %s" #: rhodecode/controllers/changeset.py:345 msgid "" @@ -71,7 +71,7 @@ msgstr "リソースにアクセスする権限がありません" #: rhodecode/controllers/error.py:103 msgid "You don't have permission to view this page" -msgstr "このページを見る権限がありません" +msgstr "このページを閲覧する権限がありません" #: rhodecode/controllers/error.py:105 msgid "The resource could not be found" @@ -285,19 +285,17 @@ msgid "An error occurred during deletion msgstr "リポジトリ %s の削除中にエラーが発生しました" #: rhodecode/controllers/settings.py:185 -#, fuzzy msgid "unlocked" -msgstr "変更可能にする" +msgstr "アンロック" #: rhodecode/controllers/settings.py:188 -#, fuzzy msgid "locked" -msgstr "変更可能にする" +msgstr "ロック" #: rhodecode/controllers/settings.py:190 -#, fuzzy, python-format +#, python-format msgid "Repository has been %s" -msgstr "" +msgstr "リポジトリは %s されています" #: rhodecode/controllers/settings.py:194 #: rhodecode/controllers/admin/repos.py:423 @@ -314,14 +312,12 @@ msgid "Statistics are disabled for this msgstr "このリポジトリの統計は無効化されています" #: rhodecode/controllers/admin/defaults.py:96 -#, fuzzy msgid "Default settings updated successfully" -msgstr "LDAP設定を更新しました" +msgstr "デフォルト設定を更新しました" #: rhodecode/controllers/admin/defaults.py:110 -#, fuzzy msgid "error occurred during update of defaults" -msgstr "ユーザー %s の更新中にエラーが発生しました" +msgstr "デフォルト設定の更新中にエラーが発生しました" #: rhodecode/controllers/admin/ldap_settings.py:50 msgid "BASE" @@ -473,7 +469,7 @@ msgstr "リポジトリ %s を作成中にエラーが発生しました" #: rhodecode/controllers/admin/repos.py:320 #, python-format msgid "Cannot delete %s it still contains attached forks" -msgstr "" +msgstr "フォークしたリポジトリが存在するため、 %s は削除できません" #: rhodecode/controllers/admin/repos.py:349 msgid "An error occurred during deletion of repository user" @@ -493,11 +489,11 @@ msgstr "キャッシュの無効化時にエラーが発生しました" #: rhodecode/controllers/admin/repos.py:443 msgid "Updated repository visibility in public journal" -msgstr "" +msgstr "公開ジャーナルでのリポジトリの可視性を更新しました" #: rhodecode/controllers/admin/repos.py:447 msgid "An error occurred during setting this repository in public journal" -msgstr "" +msgstr "このリポジトリの公開ジャーナルの設定中にエラーが発生しました" #: rhodecode/controllers/admin/repos.py:452 rhodecode/model/validators.py:300 msgid "Token mismatch" @@ -750,7 +746,7 @@ msgstr "バイナリファイル" #: rhodecode/lib/diffs.py:90 msgid "Changeset was too big and was cut off, use diff menu to display this diff" -msgstr "" +msgstr "チェンジセットが大きすぎるため省略しました。差分を表示する場合は差分メニューを使用してください" #: rhodecode/lib/diffs.py:100 msgid "No changes detected" @@ -770,14 +766,14 @@ msgid "False" msgstr "False" #: rhodecode/lib/helpers.py:530 -#, fuzzy, python-format +#, python-format msgid "Deleted branch: %s" -msgstr "リポジトリ %s を削除しました" +msgstr "削除されたブランチ: %s" #: rhodecode/lib/helpers.py:533 -#, fuzzy, python-format +#, python-format msgid "Created tag: %s" -msgstr "ユーザー %s を作成しました" +msgstr "作成したタグ: %s" #: rhodecode/lib/helpers.py:546 msgid "Changeset not found" @@ -794,21 +790,21 @@ msgstr "比較の表示" #: rhodecode/lib/helpers.py:615 msgid "and" -msgstr "" +msgstr "と" #: rhodecode/lib/helpers.py:616 #, python-format msgid "%s more" -msgstr "" +msgstr "%s 以上" #: rhodecode/lib/helpers.py:617 rhodecode/templates/changelog/changelog.html:51 msgid "revisions" msgstr "リビジョン" #: rhodecode/lib/helpers.py:641 -#, fuzzy, python-format +#, python-format msgid "fork name %s" -msgstr "" +msgstr "フォーク名 %s" #: rhodecode/lib/helpers.py:658 #: rhodecode/templates/pullrequests/pullrequest_show.html:4 @@ -959,9 +955,9 @@ msgid "%s ago" msgstr "%s 前" #: rhodecode/lib/utils2.py:428 -#, fuzzy, python-format +#, python-format msgid "in %s and %s" -msgstr "%s と %s 前" +msgstr "" #: rhodecode/lib/utils2.py:431 #, python-format @@ -1084,39 +1080,39 @@ msgid "Enter %(min)i characters or more" msgstr "%(min)i 文字以上必要です" #: rhodecode/model/notification.py:220 -#, fuzzy, python-format +#, python-format msgid "commented on commit at %(when)s" -msgstr "" +msgstr "コミットにコメント %(when)s" #: rhodecode/model/notification.py:221 #, python-format msgid "sent message at %(when)s" -msgstr "" +msgstr "メッセージを送信 %(when)s" #: rhodecode/model/notification.py:222 #, python-format msgid "mentioned you at %(when)s" -msgstr "" +msgstr "Mention %(when)s" #: rhodecode/model/notification.py:223 #, python-format msgid "registered in RhodeCode at %(when)s" -msgstr "" +msgstr "RhodeCodeに登録 %(when)s" #: rhodecode/model/notification.py:224 -#, fuzzy, python-format +#, python-format msgid "opened new pull request at %(when)s" -msgstr "" +msgstr "新しいプルリクエストを作成 %(when)s" #: rhodecode/model/notification.py:225 -#, fuzzy, python-format +#, python-format msgid "commented on pull request at %(when)s" -msgstr "" +msgstr "プルリクエストにコメント %(when)s" #: rhodecode/model/pull_request.py:90 #, python-format msgid "%(user)s wants you to review pull request #%(pr_id)s" -msgstr "" +msgstr "%(user)s がプリリクエスト #%(pr_id)s のレビューを求めています" #: rhodecode/model/scm.py:542 msgid "latest tip" @@ -1129,11 +1125,11 @@ msgstr "新規ユーザー登録" #: rhodecode/model/user.py:257 rhodecode/model/user.py:281 #: rhodecode/model/user.py:303 msgid "You can't Edit this user since it's crucial for entire application" -msgstr "" +msgstr "アプリケーション全体にとって重要なユーザなため、編集出来ません" #: rhodecode/model/user.py:327 msgid "You can't remove this user since it's crucial for entire application" -msgstr "" +msgstr "アプリケーション全体にとって重要なユーザなため、削除できません" #: rhodecode/model/user.py:333 #, python-format @@ -1144,7 +1140,7 @@ msgstr "" #: rhodecode/model/validators.py:36 rhodecode/model/validators.py:37 msgid "Value cannot be an empty list" -msgstr "" +msgstr "空のリストには出来ません" #: rhodecode/model/validators.py:83 #, python-format @@ -1244,16 +1240,15 @@ msgstr "無効なクローンURIです" #: rhodecode/model/validators.py:433 msgid "Invalid clone url, provide a valid clone http(s)/svn+http(s) url" -msgstr "" +msgstr "無効なクローンURIです。有効な http(s)/svn+http(s) のURIを指定してください" #: rhodecode/model/validators.py:458 msgid "Fork have to be the same type as parent" msgstr "フォークは親と同じタイプの必要があります" #: rhodecode/model/validators.py:473 -#, fuzzy msgid "You don't have permissions to create repository in this group" -msgstr "このページを見る権限がありません" +msgstr "このグループでリポジトリを作成する権限がありません" #: rhodecode/model/validators.py:498 msgid "This username or users group name is not valid" @@ -1617,22 +1612,20 @@ msgid "Admin journal" msgstr "管理者ジャーナル" #: rhodecode/templates/admin/admin.html:10 -#, fuzzy msgid "journal filter..." -msgstr "クイックフィルタ..." +msgstr "ジャーナルフィルタ..." #: rhodecode/templates/admin/admin.html:12 #: rhodecode/templates/journal/journal.html:11 -#, fuzzy msgid "filter" -msgstr "ファイル" +msgstr "フィルタ" #: rhodecode/templates/admin/admin.html:13 #: rhodecode/templates/journal/journal.html:12 #, python-format msgid "%s entry" msgid_plural "%s entries" -msgstr[0] "" +msgstr[0] "%s エントリ" #: rhodecode/templates/admin/admin_log.html:6 #: rhodecode/templates/admin/repos/repos.html:74 @@ -1668,14 +1661,12 @@ msgstr "まだアクションがありません" #: rhodecode/templates/admin/defaults/defaults.html:5 #: rhodecode/templates/admin/defaults/defaults.html:25 -#, fuzzy msgid "Repositories defaults" -msgstr "リポジトリグループ" +msgstr "リポジトリのデフォルト設定" #: rhodecode/templates/admin/defaults/defaults.html:11 -#, fuzzy msgid "Defaults" -msgstr "default" +msgstr "デフォルト設定" #: rhodecode/templates/admin/defaults/defaults.html:35 #: rhodecode/templates/admin/repos/repo_add_base.html:38 @@ -1722,7 +1713,7 @@ msgstr "ロックを有効にする" #: rhodecode/templates/admin/defaults/defaults.html:79 #: rhodecode/templates/admin/repos/repo_edit.html:116 msgid "Enable lock-by-pulling on repository." -msgstr "" +msgstr "リポジトリのpullのロックを有効にします" #: rhodecode/templates/admin/defaults/defaults.html:84 #: rhodecode/templates/admin/ldap/ldap.html:89 @@ -2076,20 +2067,19 @@ msgstr "リポジトリのキャッシュを無効化してもよろしいですか?" msgid "" "Manually invalidate cache for this repository. On first access repository" " will be cached again" -msgstr "" +msgstr "このリポジトリのキャッシュを手動で無効化します。リポジトリへの初回アクセス時に再びキャッシュされます。" #: rhodecode/templates/admin/repos/repo_edit.html:198 msgid "List of cached values" -msgstr "" +msgstr "キャッシュしている値の一覧" #: rhodecode/templates/admin/repos/repo_edit.html:201 msgid "Prefix" -msgstr "" +msgstr "プレフィックス" #: rhodecode/templates/admin/repos/repo_edit.html:202 -#, fuzzy msgid "Key" -msgstr "APIキー" +msgstr "キー" #: rhodecode/templates/admin/repos/repo_edit.html:203 #: rhodecode/templates/admin/users/user_add.html:86 @@ -2146,7 +2136,7 @@ msgstr "リポジトリはロックされていません" #: rhodecode/templates/admin/repos/repo_edit.html:252 msgid "Force locking on repository. Works only when anonymous access is disabled" -msgstr "" +msgstr "リポジトリを強制ロックします。匿名アクセスが無効になっている場合のみ動作します。" #: rhodecode/templates/admin/repos/repo_edit.html:259 msgid "Set as fork of" @@ -2173,14 +2163,13 @@ msgstr "このリポジトリを削除しますか?" #: rhodecode/templates/admin/repos/repo_edit.html:282 #: rhodecode/templates/settings/repo_settings.html:119 -#, fuzzy msgid "" "This repository will be renamed in a special way in order to be " "unaccesible for RhodeCode and VCS systems. If you need fully delete it " "from file system please do it manually" msgstr "" -"このリポジトリはRhodeCodeとVCSシステムからアクセスされないような名前に、特別な方法で変更されます。\n" -"もし、ファイルシステムから完全に削除したい場合、手動で行ってください" +"このリポジトリはRhodeCodeとVCSシステムからアクセス出来ないようにするために特別な方法でリネームされます。\n" +"完全な削除が必要な場合はファイルシステムから手動で削除してください" #: rhodecode/templates/admin/repos/repo_edit_perms.html:3 #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:3 @@ -2250,7 +2239,7 @@ msgstr "リポジトリ管理" #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:73 msgid "apply to children" -msgstr "" +msgstr "子リポジトリにも適用" #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:74 msgid "" @@ -2356,10 +2345,10 @@ msgid "delete" msgstr "削除" #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:55 -#, fuzzy, python-format +#, python-format msgid "Confirm to delete this group: %s with %s repository" msgid_plural "Confirm to delete this group: %s with %s repositories" -msgstr[0] "" +msgstr[0] "このグループを削除してもよろしいですか?: %s %s リポジトリ" #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:63 msgid "There are no repositories groups yet" @@ -2394,11 +2383,11 @@ msgstr "フックの削除に失敗しました" #: rhodecode/templates/admin/settings/settings.html:24 msgid "Remap and rescan repositories" -msgstr "" +msgstr "リポジトリの再マッピングと再スキャン" #: rhodecode/templates/admin/settings/settings.html:32 msgid "rescan option" -msgstr "" +msgstr "再スキャンオプション" #: rhodecode/templates/admin/settings/settings.html:38 msgid "" @@ -2464,13 +2453,12 @@ msgid "Visualisation settings" msgstr "表示の設定" #: rhodecode/templates/admin/settings/settings.html:127 -#, fuzzy msgid "General" -msgstr "有効にする" +msgstr "一般" #: rhodecode/templates/admin/settings/settings.html:132 msgid "Use lightweight dashboard" -msgstr "" +msgstr "軽量ダッシュボードを使用" #: rhodecode/templates/admin/settings/settings.html:139 msgid "Icons" @@ -2508,7 +2496,7 @@ msgstr "VCSの操作にSSLを必須とする" msgid "" "RhodeCode will require SSL for pushing or pulling. If SSL is missing it " "will return HTTP Error 406: Not Acceptable" -msgstr "" +msgstr "RhodeCodeはPushとPullにSSLを要求します。もしSSLでない場合、HTTP Error 406: Not Acceptalbeを返します" #: rhodecode/templates/admin/settings/settings.html:209 msgid "Hooks" @@ -2561,12 +2549,15 @@ msgid "" "This a crucial application setting. If you are really sure you need to " "change this, you must restart application in order to make this setting " "take effect. Click this label to unlock." -msgstr "これはアプリケーションの重要な設定です。本当に変更が必要でしょうか。もし、変更した場合、変更を反映さ競るためにアプリケーションを再起動する必要があります。変更可能にするにはこのラベルをクリックして下さい" +msgstr "" +"これはアプリケーションの重要な設定です。本当に変更が必要でしょうか。" +"もし、変更した場合、変更を反映さ競るためにアプリケーションを再起動する必要があります。" +"アンロックにするにはこのラベルをクリックして下さい" #: rhodecode/templates/admin/settings/settings.html:262 #: rhodecode/templates/base/base.html:221 msgid "unlock" -msgstr "変更可能にする" +msgstr "アンロック" #: rhodecode/templates/admin/settings/settings.html:263 msgid "" @@ -2869,19 +2860,16 @@ msgid "Group members" msgstr "グループメンバー" #: rhodecode/templates/admin/users_groups/users_group_edit.html:163 -#, fuzzy msgid "No members yet" -msgstr "メンバー" +msgstr "まだメンバーがいません" #: rhodecode/templates/admin/users_groups/users_group_edit.html:171 -#, fuzzy msgid "Permissions defined for this group" -msgstr "権限管理" +msgstr "このリポジトリの権限設定" #: rhodecode/templates/admin/users_groups/users_group_edit.html:178 -#, fuzzy msgid "No permissions set yet" -msgstr "権限のコピー" +msgstr "まだ権限設定がありません" #: rhodecode/templates/admin/users_groups/users_groups.html:5 msgid "Users groups administration" @@ -2992,9 +2980,8 @@ msgstr "オプション" #: rhodecode/templates/base/base.html:204 #: rhodecode/templates/base/base.html:206 -#, fuzzy msgid "repository settings" -msgstr "リポジトリ作成" +msgstr "リポジトリ設定" #: rhodecode/templates/base/base.html:210 #: rhodecode/templates/data_table/_dt_elements.html:80 @@ -3017,9 +3004,8 @@ msgid "search" msgstr "検索" #: rhodecode/templates/base/base.html:223 -#, fuzzy msgid "lock" -msgstr "変更可能にする" +msgstr "ロック" #: rhodecode/templates/base/base.html:234 msgid "repositories groups" @@ -3034,9 +3020,8 @@ msgid "permissions" msgstr "権限" #: rhodecode/templates/base/base.html:239 -#, fuzzy msgid "defaults" -msgstr "default" +msgstr "デフォルト設定" #: rhodecode/templates/base/base.html:240 msgid "settings" @@ -3087,13 +3072,12 @@ msgid "no matching files" msgstr "マッチするファイルはありません" #: rhodecode/templates/base/root.html:51 -#, fuzzy msgid "Open new pull request for selected changesets" -msgstr "新しいプルリクエストを作成" +msgstr "選択したチェンジセットから新しいプルリクエストを作成" #: rhodecode/templates/base/root.html:52 msgid "Show selected changes __S -> __E" -msgstr "" +msgstr "選択した変更 __S -> __E を表示" #: rhodecode/templates/base/root.html:53 msgid "Selection link" @@ -3143,9 +3127,8 @@ msgid_plural "showing %d out of %d revis msgstr[0] "" #: rhodecode/templates/changelog/changelog.html:37 -#, fuzzy msgid "Clear selection" -msgstr "検索設定" +msgstr "選択を解除" #: rhodecode/templates/changelog/changelog.html:40 #: rhodecode/templates/forks/forks_data.html:19 @@ -3154,9 +3137,8 @@ msgid "compare fork with %s" msgstr "%s とフォークを比較" #: rhodecode/templates/changelog/changelog.html:40 -#, fuzzy msgid "Compare fork with parent" -msgstr "%s とフォークを比較" +msgstr "フォークを比較" #: rhodecode/templates/changelog/changelog.html:49 msgid "Show" @@ -3259,7 +3241,7 @@ msgstr "チェンジセット" #: rhodecode/templates/changeset/changeset.html:52 msgid "No children" -msgstr "" +msgstr "子リビジョンはありません" #: rhodecode/templates/changeset/changeset.html:70 #: rhodecode/templates/changeset/diff_block.html:20 @@ -3267,9 +3249,8 @@ msgid "raw diff" msgstr "差分を表示" #: rhodecode/templates/changeset/changeset.html:71 -#, fuzzy msgid "patch diff" -msgstr "差分を表示" +msgstr "パッチとして差分を表示" #: rhodecode/templates/changeset/changeset.html:72 #: rhodecode/templates/changeset/diff_block.html:21 @@ -3293,18 +3274,18 @@ msgstr[0] "(%d インライン)" #: rhodecode/templates/changeset/changeset.html:122 #: rhodecode/templates/compare/compare_diff.html:44 #: rhodecode/templates/pullrequests/pullrequest_show.html:76 -#, fuzzy, python-format +#, python-format msgid "%s file changed" msgid_plural "%s files changed" -msgstr[0] "" +msgstr[0] "%s ファイルに影響" #: rhodecode/templates/changeset/changeset.html:124 #: rhodecode/templates/compare/compare_diff.html:46 #: rhodecode/templates/pullrequests/pullrequest_show.html:78 -#, fuzzy, python-format +#, python-format msgid "%s file changed with %s insertions and %s deletions" msgid_plural "%s files changed with %s insertions and %s deletions" -msgstr[0] "%s ファイルに影響。 %s 個の追加と %s 個の削除:" +msgstr[0] "%s ファイルに影響。 %s 個の追加と %s 個の削除" #: rhodecode/templates/changeset/changeset_file_comment.html:42 msgid "Submitting..." @@ -3349,7 +3330,7 @@ msgstr "コメントを残す" #: rhodecode/templates/changeset/changeset_file_comment.html:125 msgid "Check this to change current status of code-review for this changeset" -msgstr "" +msgstr "チェックするとチェンジセットの現在のコードレビューステータスを変更出来ます" #: rhodecode/templates/changeset/changeset_file_comment.html:125 msgid "change status" @@ -3370,9 +3351,8 @@ msgid "Compare View" msgstr "比較ビュー" #: rhodecode/templates/changeset/changeset_range.html:29 -#, fuzzy msgid "Show combined compare" -msgstr "インラインコメントを表示" +msgstr "結合した比較ビューを表示" #: rhodecode/templates/changeset/changeset_range.html:54 msgid "Files affected" @@ -3380,7 +3360,7 @@ msgstr "影響のあるファイル" #: rhodecode/templates/changeset/diff_block.html:19 msgid "show full diff for this file" -msgstr "" +msgstr "このファイルの全差分を表示" #: rhodecode/templates/changeset/diff_block.html:27 msgid "show inline comments" @@ -3392,16 +3372,15 @@ msgstr "チェンジセットはありません" #: rhodecode/templates/compare/compare_diff.html:37 #: rhodecode/templates/pullrequests/pullrequest_show.html:69 -#, fuzzy, python-format +#, python-format msgid "Showing %s commit" msgid_plural "Showing %s commits" -msgstr[0] "" +msgstr[0] "%s コミットを表示" #: rhodecode/templates/compare/compare_diff.html:52 #: rhodecode/templates/pullrequests/pullrequest_show.html:84 -#, fuzzy msgid "No files" -msgstr "ファイル" +msgstr "ファイルはありません" #: rhodecode/templates/data_table/_dt_elements.html:39 #: rhodecode/templates/data_table/_dt_elements.html:41 @@ -3455,40 +3434,37 @@ msgid "Confirm to delete this user: %s" msgstr "このユーザーを本当に削除してよろしいですか?: %s" #: rhodecode/templates/email_templates/changeset_comment.html:10 -#, fuzzy msgid "New status$" -msgstr "ステータスを変更する" +msgstr "新しいステータス$" #: rhodecode/templates/email_templates/main.html:8 -#, fuzzy msgid "This is a notification from RhodeCode." -msgstr "RhodeCodeからの通知があります" +msgstr "RhodeCodeからの通知です" #: rhodecode/templates/email_templates/password_reset.html:4 msgid "Hello" -msgstr "" +msgstr "こんにちは" #: rhodecode/templates/email_templates/password_reset.html:6 msgid "We received a request to create a new password for your account." -msgstr "" +msgstr "あなたのアカウントの新しいパスワードの生成リクエストを受け取りました。" #: rhodecode/templates/email_templates/password_reset.html:8 msgid "You can generate it by clicking following URL" -msgstr "" +msgstr "下のURLをクリックすることで再生成が行えます。" #: rhodecode/templates/email_templates/password_reset.html:12 msgid "If you didn't request new password please ignore this email." -msgstr "" +msgstr "新しいパスワードのリクエストをしていない場合は、このメールを無視して下さい。" #: rhodecode/templates/email_templates/pull_request.html:4 #, python-format msgid "" "User %s opened pull request for repository %s and wants you to review " "changes." -msgstr "" +msgstr "ユーザ %s がリポジトリ %s で新しいプルリクエストを作成しました。変更をレビューしてください。" #: rhodecode/templates/email_templates/pull_request.html:5 -#, fuzzy msgid "title" msgstr "タイトル" @@ -3499,35 +3475,32 @@ msgstr "説明" #: rhodecode/templates/email_templates/pull_request.html:11 msgid "revisions for reviewing" -msgstr "" +msgstr "レビュー対象のリビジョン" #: rhodecode/templates/email_templates/pull_request.html:18 -#, fuzzy msgid "View this pull request here" -msgstr "このプルリクエストにレビュアーを追加" +msgstr "このプルリクエストを閲覧する" #: rhodecode/templates/email_templates/pull_request_comment.html:4 -#, fuzzy, python-format +#, python-format msgid "User %s commented on pull request #%s for repository %s" -msgstr "" +msgstr "ユーザ %s がプルリクエスト #%s (リポジトリ %s) にコメントしました。" #: rhodecode/templates/email_templates/pull_request_comment.html:10 -#, fuzzy msgid "New status" -msgstr "ステータスを変更する" +msgstr "新しいステータス" #: rhodecode/templates/email_templates/pull_request_comment.html:14 msgid "View this comment here" -msgstr "" +msgstr "このコメントを閲覧する" #: rhodecode/templates/email_templates/registration.html:4 -#, fuzzy msgid "A new user have registered in RhodeCode" -msgstr "rhodecodeへの登録を受け付けました" +msgstr "新しいユーザがRhodeCodeへ登録しました" #: rhodecode/templates/email_templates/registration.html:9 msgid "View this user here" -msgstr "" +msgstr "このユーザを閲覧する" #: rhodecode/templates/errors/error_document.html:46 #, python-format @@ -3609,7 +3582,7 @@ msgstr "変更をコミット" #: rhodecode/templates/files/files_browser.html:13 msgid "view" -msgstr "表示" +msgstr "閲覧" #: rhodecode/templates/files/files_browser.html:14 msgid "previous revision" @@ -3697,9 +3670,8 @@ msgid "show at revision" msgstr "このリビジョンを見る" #: rhodecode/templates/files/files_history_box.html:11 -#, fuzzy msgid "show full history" -msgstr "ファイル一覧を読み込み中..." +msgstr "すべての履歴を表示" #: rhodecode/templates/files/files_history_box.html:16 #, python-format @@ -3708,9 +3680,8 @@ msgid_plural "%s authors" msgstr[0] "%s 作成者" #: rhodecode/templates/files/files_source.html:6 -#, fuzzy msgid "Load file history" -msgstr "ファイル一覧を読み込み中..." +msgstr "ファイルの履歴を読み込む" #: rhodecode/templates/files/files_source.html:21 msgid "show source" @@ -3909,7 +3880,7 @@ msgstr "%s にクローズ" #: rhodecode/templates/pullrequests/pullrequest_show.html:23 #, python-format msgid "with status %s" -msgstr "" +msgstr "ステータス: %s" #: rhodecode/templates/pullrequests/pullrequest_show.html:31 msgid "Status" @@ -3930,9 +3901,8 @@ msgid_plural "%d reviewers" msgstr[0] "%d レビュアー" #: rhodecode/templates/pullrequests/pullrequest_show.html:50 -#, fuzzy msgid "pull request was reviewed by all reviewers" -msgstr "プルリクエストレビュアー" +msgstr "プルリクエストはすべてのレビュアーにレビューされました" #: rhodecode/templates/pullrequests/pullrequest_show.html:58 msgid "Created on" @@ -3943,9 +3913,8 @@ msgid "Compare view" msgstr "比較ビュー" #: rhodecode/templates/pullrequests/pullrequest_show.html:112 -#, fuzzy msgid "reviewer" -msgstr "%d レビュアー" +msgstr "レビュアー" #: rhodecode/templates/pullrequests/pullrequest_show_all.html:4 msgid "all pull requests" @@ -4012,12 +3981,10 @@ msgid "%s Settings" msgstr "%s 設定" #: rhodecode/templates/settings/repo_settings.html:102 -#, fuzzy msgid "Delete repository" -msgstr "リポジトリを[削除]" +msgstr "リポジトリを削除" #: rhodecode/templates/settings/repo_settings.html:109 -#, fuzzy msgid "Remove repo" msgstr "削除" @@ -4080,19 +4047,18 @@ msgid "ATOM" msgstr "ATOM" #: rhodecode/templates/summary/summary.html:70 -#, fuzzy, python-format +#, python-format msgid "Repository locked by %s" -msgstr "" +msgstr "リポジトリは %s によってロックされました" #: rhodecode/templates/summary/summary.html:72 -#, fuzzy msgid "Repository unlocked" msgstr "リポジトリはロックされていません" #: rhodecode/templates/summary/summary.html:91 #, python-format msgid "Non changable ID %s" -msgstr "" +msgstr "変更不能ID %s" #: rhodecode/templates/summary/summary.html:96 msgid "public" @@ -4100,7 +4066,7 @@ msgstr "公開" #: rhodecode/templates/summary/summary.html:104 msgid "remote clone" -msgstr "" +msgstr "リモートクローン" #: rhodecode/templates/summary/summary.html:125 msgid "Contact" @@ -4171,7 +4137,7 @@ msgstr "クイックスタート" #: rhodecode/templates/summary/summary.html:243 #, python-format msgid "Readme file at revision '%s'" -msgstr "" +msgstr "リビジョン '%s' のReadmeファイル" #: rhodecode/templates/summary/summary.html:246 msgid "Permalink to this readme" @@ -4220,9 +4186,8 @@ msgid "%s Tags" msgstr "%s タグ" #: rhodecode/templates/tags/tags.html:29 -#, fuzzy msgid "Compare tags" -msgstr "比較" +msgstr "タグの比較" #~ msgid "" #~ "%s repository is not mapped to db" diff --git a/rhodecode/i18n/pl/LC_MESSAGES/rhodecode.po b/rhodecode/i18n/pl/LC_MESSAGES/rhodecode.po --- a/rhodecode/i18n/pl/LC_MESSAGES/rhodecode.po +++ b/rhodecode/i18n/pl/LC_MESSAGES/rhodecode.po @@ -3,20 +3,21 @@ # This file is distributed under the same license as the rhodecode project. # FIRST AUTHOR , 2010. # Nemcio , 2012. -# Nemo , 2012. +# Nemo , 2012, 2013. msgid "" msgstr "" "Project-Id-Version: rhodecode 0.1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2012-12-14 04:19+0100\n" -"PO-Revision-Date: 2012-11-25 03:42+0200\n" -"Last-Translator: Nemo \n" +"PO-Revision-Date: 2013-01-18 18:12+0100\n" +"Last-Translator: Nemcio \n" "Language-Team: Test\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && " -"(n%100<10 || n%100>=20) ? 1 : 2)\n" +"Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Virtaal 0.7.1\n" "Generated-By: Babel 0.9.6\n" #: rhodecode/controllers/changelog.py:95 @@ -27,7 +28,8 @@ msgstr "Wszystkie gałęzie" msgid "show white space" msgstr "pokazuj spacje" -#: rhodecode/controllers/changeset.py:90 rhodecode/controllers/changeset.py:97 +#: rhodecode/controllers/changeset.py:90 +#: rhodecode/controllers/changeset.py:97 msgid "ignore white space" msgstr "ignoruj pokazywanie spacji" @@ -43,12 +45,8 @@ msgid "Status change -> %s" msgstr "Zmiana statusu -> %s" #: rhodecode/controllers/changeset.py:345 -msgid "" -"Changing status on a changeset associated witha closed pull request is " -"not allowed" -msgstr "" -"Zmiana statusu na grupy zmian powiązania łączy zamkniętego wniosku jest " -"niedozwolona" +msgid "Changing status on a changeset associated witha closed pull request is not allowed" +msgstr "Zmiana statusu na grupy zmian powiązania łączy zamkniętego wniosku jest niedozwolona" #: rhodecode/controllers/compare.py:75 #: rhodecode/controllers/pullrequests.py:121 @@ -62,9 +60,7 @@ msgstr "Strona główna" #: rhodecode/controllers/error.py:98 msgid "The request could not be understood by the server due to malformed syntax." -msgstr "" -"Wniosek nie może być rozumiany przez serwer z powodu zniekształconej " -"składni." +msgstr "Wniosek nie może być rozumiany przez serwer z powodu zniekształconej składni." #: rhodecode/controllers/error.py:101 msgid "Unauthorized access to resource" @@ -79,12 +75,8 @@ msgid "The resource could not be found" msgstr "Zasób nie został znaleziony" #: rhodecode/controllers/error.py:107 -msgid "" -"The server encountered an unexpected condition which prevented it from " -"fulfilling the request." -msgstr "" -"Serwer napotkał niespodziewany warunek, który uniemożliwia jej spełnienie" -" żądania." +msgid "The server encountered an unexpected condition which prevented it from fulfilling the request." +msgstr "Serwer napotkał niespodziewany warunek, który uniemożliwia jej spełnienie żądania." #: rhodecode/controllers/feed.py:52 #, python-format @@ -119,7 +111,8 @@ msgstr "Kliknij tutaj, by dodać nowy plik" msgid "There are no files yet %s" msgstr "Brak plików %s" -#: rhodecode/controllers/files.py:265 rhodecode/controllers/files.py:325 +#: rhodecode/controllers/files.py:265 +#: rhodecode/controllers/files.py:325 #, python-format msgid "This repository is has been locked by %s on %s" msgstr "Repozytorium zostało zablokowane przez %s na %s" @@ -133,12 +126,14 @@ msgstr "Edytowanie %s w RhodeCode" msgid "No changes" msgstr "Bez zmian" -#: rhodecode/controllers/files.py:308 rhodecode/controllers/files.py:372 +#: rhodecode/controllers/files.py:308 +#: rhodecode/controllers/files.py:372 #, python-format msgid "Successfully committed to %s" msgstr "Committ wykonany do %s" -#: rhodecode/controllers/files.py:313 rhodecode/controllers/files.py:378 +#: rhodecode/controllers/files.py:313 +#: rhodecode/controllers/files.py:378 msgid "Error occurred during commit" msgstr "Wystąpił błąd w trakcie zatwierdzania" @@ -178,13 +173,17 @@ msgstr "Nieznany typ archiwum" msgid "Changesets" msgstr "Różnice" -#: rhodecode/controllers/files.py:565 rhodecode/controllers/pullrequests.py:74 -#: rhodecode/controllers/summary.py:236 rhodecode/model/scm.py:550 +#: rhodecode/controllers/files.py:565 +#: rhodecode/controllers/pullrequests.py:74 +#: rhodecode/controllers/summary.py:236 +#: rhodecode/model/scm.py:550 msgid "Branches" msgstr "Gałęzie" -#: rhodecode/controllers/files.py:566 rhodecode/controllers/pullrequests.py:78 -#: rhodecode/controllers/summary.py:237 rhodecode/model/scm.py:561 +#: rhodecode/controllers/files.py:566 +#: rhodecode/controllers/pullrequests.py:78 +#: rhodecode/controllers/summary.py:237 +#: rhodecode/model/scm.py:561 msgid "Tags" msgstr "Etykiety" @@ -198,11 +197,13 @@ msgstr "gałęzi %s w repozytorium %s" msgid "An error occurred during repository forking %s" msgstr "Wystąpił błąd podczas rozgałęzienia %s repozytorium" -#: rhodecode/controllers/journal.py:218 rhodecode/controllers/journal.py:261 +#: rhodecode/controllers/journal.py:218 +#: rhodecode/controllers/journal.py:261 msgid "public journal" msgstr "Dziennik publiczny" -#: rhodecode/controllers/journal.py:222 rhodecode/controllers/journal.py:265 +#: rhodecode/controllers/journal.py:222 +#: rhodecode/controllers/journal.py:265 #: rhodecode/templates/base/base.html:232 #: rhodecode/templates/journal/journal.html:12 msgid "journal" @@ -217,12 +218,11 @@ msgid "Your password reset link was sent msgstr "Twój link zresetowania hasła został wysłany" #: rhodecode/controllers/login.py:184 -msgid "" -"Your password reset was successful, new password has been sent to your " -"email" +msgid "Your password reset was successful, new password has been sent to your email" msgstr "Twoje hasło zostało zresetowane, nowe hasło zostanie wysłane na e-mail" -#: rhodecode/controllers/pullrequests.py:76 rhodecode/model/scm.py:556 +#: rhodecode/controllers/pullrequests.py:76 +#: rhodecode/model/scm.py:556 msgid "Bookmarks" msgstr "Zakładki" @@ -247,8 +247,9 @@ msgid "Successfully deleted pull request msgstr "Prośba o skasowanie połączenia gałęzi została wykonana prawidłowo" #: rhodecode/controllers/pullrequests.py:452 +#, fuzzy msgid "Closing pull request on other statuses than rejected or approved forbidden" -msgstr "" +msgstr "Zamknij wszystkie wnioski połączenia gałęzi innych stanów niż odrzucony, zatwierdzony lub zabroniony" #: rhodecode/controllers/search.py:134 msgid "Invalid search query. Try quoting it." @@ -315,14 +316,12 @@ msgid "Statistics are disabled for this msgstr "Statystyki są wyłączone dla tego repozytorium" #: rhodecode/controllers/admin/defaults.py:96 -#, fuzzy msgid "Default settings updated successfully" -msgstr "Ustawienia LDAP zostały zaktualizowane" +msgstr "Domyślne ustawienia zostały pomyślnie zaktualizowane" #: rhodecode/controllers/admin/defaults.py:110 -#, fuzzy msgid "error occurred during update of defaults" -msgstr "wystąpił błąd podczas aktualizacji użytkownika %s" +msgstr "wystąpił błąd podczas aktualizacji wartości domyślnych" #: rhodecode/controllers/admin/ldap_settings.py:50 msgid "BASE" @@ -500,7 +499,8 @@ msgstr "Zaktualizowano widoczność stron w publicznym dzienniku" msgid "An error occurred during setting this repository in public journal" msgstr "Wystąpił błąd podczas ustawiania tego repozytorium w dzienniku publicznym" -#: rhodecode/controllers/admin/repos.py:452 rhodecode/model/validators.py:300 +#: rhodecode/controllers/admin/repos.py:452 +#: rhodecode/model/validators.py:300 msgid "Token mismatch" msgstr "Niezgodność tokenu" @@ -576,9 +576,7 @@ msgstr "Wystąpił błąd podczas usuwania grup i grup użytkowników" #: rhodecode/controllers/admin/settings.py:123 #, python-format msgid "Repositories successfully rescanned added: %s,removed: %s" -msgstr "" -"Repozytoria z powodzeniem zostały ponownie zeskanowane dodano: %s, " -"usunięto: %s" +msgstr "Repozytoria z powodzeniem zostały ponownie zeskanowane dodano: %s, usunięto: %s" #: rhodecode/controllers/admin/settings.py:131 msgid "Whoosh reindex task scheduled" @@ -623,9 +621,7 @@ msgstr "E-mail został wysłany" #: rhodecode/controllers/admin/settings.py:399 msgid "You can't edit this user since it's crucial for entire application" -msgstr "" -"Nie możesz edytować tego użytkownika ponieważ jest kluczowy dla całej " -"aplikacji" +msgstr "Nie możesz edytować tego użytkownika ponieważ jest kluczowy dla całej aplikacji" #: rhodecode/controllers/admin/settings.py:430 msgid "Your account was updated successfully" @@ -755,9 +751,7 @@ msgstr "plik binarny" #: rhodecode/lib/diffs.py:90 msgid "Changeset was too big and was cut off, use diff menu to display this diff" -msgstr "" -"Lista zmian była zbyt duża i została obcięta, użyj menu porównań żeby " -"wyświetlić różnice" +msgstr "Lista zmian była zbyt duża i została obcięta, użyj menu porównań żeby wyświetlić różnice" #: rhodecode/lib/diffs.py:100 msgid "No changes detected" @@ -808,7 +802,8 @@ msgstr "i" msgid "%s more" msgstr "%s więcej" -#: rhodecode/lib/helpers.py:617 rhodecode/templates/changelog/changelog.html:51 +#: rhodecode/lib/helpers.py:617 +#: rhodecode/templates/changelog/changelog.html:51 msgid "revisions" msgstr "rewizja" @@ -828,7 +823,8 @@ msgstr "Połączonych gałęzi #%s" msgid "[deleted] repository" msgstr "[usunięte] repozytorium" -#: rhodecode/lib/helpers.py:666 rhodecode/lib/helpers.py:676 +#: rhodecode/lib/helpers.py:666 +#: rhodecode/lib/helpers.py:676 msgid "[created] repository" msgstr "[utworzone] repozytorium" @@ -836,11 +832,13 @@ msgstr "[utworzone] repozytorium" msgid "[created] repository as fork" msgstr "[utworzone] repozytorium jako rozgałęzienie" -#: rhodecode/lib/helpers.py:670 rhodecode/lib/helpers.py:678 +#: rhodecode/lib/helpers.py:670 +#: rhodecode/lib/helpers.py:678 msgid "[forked] repository" msgstr "[rozgałęzione] repozytorium" -#: rhodecode/lib/helpers.py:672 rhodecode/lib/helpers.py:680 +#: rhodecode/lib/helpers.py:672 +#: rhodecode/lib/helpers.py:680 msgid "[updated] repository" msgstr "[zaktualizowane] repozytorium" @@ -911,14 +909,8 @@ msgstr "Brak Plików" #: rhodecode/lib/helpers.py:1163 #, python-format -msgid "" -"%s repository is not mapped to db perhaps it was created or renamed from " -"the filesystem please run the application again in order to rescan " -"repositories" -msgstr "" -"%s repozytorium nie jest mapowane do db może zostało utworzone lub " -"zmienione z systemie plików proszę uruchomić aplikację ponownie, aby " -"ponownie przeskanować repozytoria" +msgid "%s repository is not mapped to db perhaps it was created or renamed from the filesystem please run the application again in order to rescan repositories" +msgstr "%s repozytorium nie jest mapowane do db może zostało utworzone lub zmienione z systemie plików proszę uruchomić aplikację ponownie, aby ponownie przeskanować repozytoria" #: rhodecode/lib/utils2.py:403 #, python-format @@ -996,83 +988,103 @@ msgstr "przed chwilą" msgid "password reset link" msgstr "łącze resetowania hasła" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1163 rhodecode/model/db.py:1183 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1163 +#: rhodecode/model/db.py:1183 msgid "Repository no access" msgstr "Brak dostępu do repozytorium" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1164 rhodecode/model/db.py:1184 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1164 +#: rhodecode/model/db.py:1184 msgid "Repository read access" msgstr "Repozytorium do odczytu" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1165 rhodecode/model/db.py:1185 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1165 +#: rhodecode/model/db.py:1185 msgid "Repository write access" msgstr "Repozytorium do zapisu" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1166 rhodecode/model/db.py:1186 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1166 +#: rhodecode/model/db.py:1186 msgid "Repository admin access" msgstr "Administracja dostępu do repozytorium" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1168 rhodecode/model/db.py:1188 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1168 +#: rhodecode/model/db.py:1188 msgid "Repositories Group no access" msgstr "Grupy repozytoriów brak dostępu" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1169 rhodecode/model/db.py:1189 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1169 +#: rhodecode/model/db.py:1189 msgid "Repositories Group read access" msgstr "Grupy repozytoriów dostęp do odczytu" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1170 rhodecode/model/db.py:1190 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1170 +#: rhodecode/model/db.py:1190 msgid "Repositories Group write access" msgstr "Grupy repozytoriów dostęp do zapisu" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1171 rhodecode/model/db.py:1191 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1171 +#: rhodecode/model/db.py:1191 msgid "Repositories Group admin access" msgstr "Repozytoria Grupy dostęp administratora" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1173 rhodecode/model/db.py:1193 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1173 +#: rhodecode/model/db.py:1193 msgid "RhodeCode Administrator" msgstr "Administrator Repo" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1174 rhodecode/model/db.py:1194 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1174 +#: rhodecode/model/db.py:1194 msgid "Repository creation disabled" msgstr "Repozytorium wyłączone" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1175 rhodecode/model/db.py:1195 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1175 +#: rhodecode/model/db.py:1195 msgid "Repository creation enabled" msgstr "Repozytorium włączone" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1176 rhodecode/model/db.py:1196 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1176 +#: rhodecode/model/db.py:1196 msgid "Repository forking disabled" msgstr "Rozwidlenie repozytorium wyłączone" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1177 rhodecode/model/db.py:1197 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1177 +#: rhodecode/model/db.py:1197 msgid "Repository forking enabled" msgstr "Rozwidlenie repozytorium włączone" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1178 rhodecode/model/db.py:1198 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1178 +#: rhodecode/model/db.py:1198 msgid "Register disabled" msgstr "Rejestracja wyłączona" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1179 rhodecode/model/db.py:1199 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1179 +#: rhodecode/model/db.py:1199 msgid "Register new user with RhodeCode with manual activation" msgstr "Rejestracja nowego użytkownika na stronie z ręczną aktywacją" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1182 rhodecode/model/db.py:1202 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1182 +#: rhodecode/model/db.py:1202 msgid "Register new user with RhodeCode with auto activation" msgstr "Rejestracja nowego użytkownika na stronie z automatyczną aktywacją" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1623 rhodecode/model/db.py:1643 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1623 +#: rhodecode/model/db.py:1643 msgid "Not Reviewed" msgstr "Brak Korekty" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1624 rhodecode/model/db.py:1644 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1624 +#: rhodecode/model/db.py:1644 msgid "Approved" msgstr "Zaakceptowano" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1625 rhodecode/model/db.py:1645 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1625 +#: rhodecode/model/db.py:1645 msgid "Rejected" msgstr "Odrzucono" -#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1626 rhodecode/model/db.py:1646 +#: rhodecode/lib/dbmigrate/schema/db_1_4_0.py:1626 +#: rhodecode/model/db.py:1646 msgid "Under Review" msgstr "Objęty Przeglądem" @@ -1146,29 +1158,23 @@ msgstr "ostatni tip" msgid "new user registration" msgstr "nowy użytkownik się zarejestrował" -#: rhodecode/model/user.py:257 rhodecode/model/user.py:281 +#: rhodecode/model/user.py:257 +#: rhodecode/model/user.py:281 #: rhodecode/model/user.py:303 msgid "You can't Edit this user since it's crucial for entire application" -msgstr "" -"Nie możesz edytować tego użytkownika ponieważ jest kluczowy dla całej " -"aplikacji" +msgstr "Nie możesz edytować tego użytkownika ponieważ jest kluczowy dla całej aplikacji" #: rhodecode/model/user.py:327 msgid "You can't remove this user since it's crucial for entire application" -msgstr "" -"Nie możesz usunąć tego użytkownika ponieważ jest kluczowy dla całej " -"aplikacji" +msgstr "Nie możesz usunąć tego użytkownika ponieważ jest kluczowy dla całej aplikacji" #: rhodecode/model/user.py:333 #, python-format -msgid "" -"user \"%s\" still owns %s repositories and cannot be removed. Switch " -"owners or remove those repositories. %s" -msgstr "" -"użytkownik \"%s\" wciąż posiada repozytoria następujące %s i nie może " -"zostać usunięty. Zmień właściciela lub usuń te repozytoria. %s" - -#: rhodecode/model/validators.py:36 rhodecode/model/validators.py:37 +msgid "user \"%s\" still owns %s repositories and cannot be removed. Switch owners or remove those repositories. %s" +msgstr "użytkownik \"%s\" wciąż posiada repozytoria następujące %s i nie może zostać usunięty. Zmień właściciela lub usuń te repozytoria. %s" + +#: rhodecode/model/validators.py:36 +#: rhodecode/model/validators.py:37 msgid "Value cannot be an empty list" msgstr "Wartość listy nie może być pusta" @@ -1183,12 +1189,8 @@ msgid "Username \"%(username)s\" is forb msgstr "Nazwa użytkownika \"%(username)s\" jest zabroniona" #: rhodecode/model/validators.py:87 -msgid "" -"Username may only contain alphanumeric characters underscores, periods or" -" dashes and must begin with alphanumeric character" -msgstr "" -"Nazwa użytkownika może zawierać tylko znaki alfanumeryczne, podkreślenia," -" kropki lub myślniki i musi zaczynać się znakiem alfanumerycznym" +msgid "Username may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character" +msgstr "Nazwa użytkownika może zawierać tylko znaki alfanumeryczne, podkreślenia, kropki lub myślniki i musi zaczynać się znakiem alfanumerycznym" #: rhodecode/model/validators.py:115 #, python-format @@ -1205,12 +1207,8 @@ msgid "Users group \"%(usersgroup)s\" al msgstr "Nazwa grupy \"%(usersgroup)s\" już istnieje" #: rhodecode/model/validators.py:137 -msgid "" -"users group name may only contain alphanumeric characters underscores, " -"periods or dashes and must begin with alphanumeric character" -msgstr "" -"Nazwa grupy może zawierać tylko znaki alfanumeryczne, podkreślenia, " -"kropki lub myślniki i musi zaczynać się znakiem alfanumerycznym" +msgid "users group name may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character" +msgstr "Nazwa grupy może zawierać tylko znaki alfanumeryczne, podkreślenia, kropki lub myślniki i musi zaczynać się znakiem alfanumerycznym" #: rhodecode/model/validators.py:175 msgid "Cannot assign this group as parent" @@ -1300,12 +1298,8 @@ msgid "e-mail \"%(email)s\" does not exi msgstr "e-mail \"%(email)s\" nie istnieje." #: rhodecode/model/validators.py:663 -msgid "" -"The LDAP Login attribute of the CN must be specified - this is the name " -"of the attribute that is equivalent to \"username\"" -msgstr "" -"Atrybut logowania CN do LDAP należy określić, jest to nazwa atrybutu, " -"który jest odpowiednikiem \"username\"" +msgid "The LDAP Login attribute of the CN must be specified - this is the name of the attribute that is equivalent to \"username\"" +msgstr "Atrybut logowania CN do LDAP należy określić, jest to nazwa atrybutu, który jest odpowiednikiem \"username\"" #: rhodecode/model/validators.py:682 #, python-format @@ -1498,7 +1492,8 @@ msgstr "Błąd danych." msgid "Loading..." msgstr "Wczytywanie..." -#: rhodecode/templates/login.html:5 rhodecode/templates/login.html:54 +#: rhodecode/templates/login.html:5 +#: rhodecode/templates/login.html:54 msgid "Sign In" msgstr "Zaloguj się" @@ -1506,7 +1501,8 @@ msgstr "Zaloguj się" msgid "Sign In to" msgstr "Zarejestruj się" -#: rhodecode/templates/login.html:31 rhodecode/templates/register.html:20 +#: rhodecode/templates/login.html:31 +#: rhodecode/templates/register.html:20 #: rhodecode/templates/admin/admin_log.html:5 #: rhodecode/templates/admin/users/user_add.html:32 #: rhodecode/templates/admin/users/user_edit.html:50 @@ -1516,7 +1512,8 @@ msgstr "Zarejestruj się" msgid "Username" msgstr "Nazwa użytkownika" -#: rhodecode/templates/login.html:40 rhodecode/templates/register.html:29 +#: rhodecode/templates/login.html:40 +#: rhodecode/templates/register.html:29 #: rhodecode/templates/admin/ldap/ldap.html:46 #: rhodecode/templates/admin/users/user_add.html:41 #: rhodecode/templates/base/base.html:92 @@ -1531,7 +1528,8 @@ msgstr "Zapamiętaj mnie" msgid "Forgot your password ?" msgstr "Zapomniałeś hasła?" -#: rhodecode/templates/login.html:63 rhodecode/templates/base/base.html:103 +#: rhodecode/templates/login.html:63 +#: rhodecode/templates/base/base.html:103 msgid "Don't have an account ?" msgstr "Nie masz konta?" @@ -1555,7 +1553,8 @@ msgstr "Zresetuj swoje hasło" msgid "Password reset link will be send to matching email address" msgstr "Link do zresetowania hasła zostanie wysłany na adres e-mail" -#: rhodecode/templates/register.html:5 rhodecode/templates/register.html:74 +#: rhodecode/templates/register.html:5 +#: rhodecode/templates/register.html:74 msgid "Sign Up" msgstr "Zarejestruj się" @@ -1646,24 +1645,22 @@ msgid "Admin journal" msgstr "Dziennik administratora" #: rhodecode/templates/admin/admin.html:10 -#, fuzzy msgid "journal filter..." -msgstr "szybki filtr..." +msgstr "szybkie wyszukiwanie..." #: rhodecode/templates/admin/admin.html:12 #: rhodecode/templates/journal/journal.html:11 -#, fuzzy msgid "filter" -msgstr "pliki" +msgstr "filtr" #: rhodecode/templates/admin/admin.html:13 #: rhodecode/templates/journal/journal.html:12 #, python-format msgid "%s entry" msgid_plural "%s entries" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" +msgstr[0] "%s wejście" +msgstr[1] "%s wejść" +msgstr[2] "%s wejść" #: rhodecode/templates/admin/admin_log.html:6 #: rhodecode/templates/admin/repos/repos.html:74 @@ -1699,14 +1696,12 @@ msgstr "Brak akcji" #: rhodecode/templates/admin/defaults/defaults.html:5 #: rhodecode/templates/admin/defaults/defaults.html:25 -#, fuzzy msgid "Repositories defaults" -msgstr "grupy w repozytorium" +msgstr "Repozytoria domyślne" #: rhodecode/templates/admin/defaults/defaults.html:11 -#, fuzzy msgid "Defaults" -msgstr "domyślne" +msgstr "Domyślne" #: rhodecode/templates/admin/defaults/defaults.html:35 #: rhodecode/templates/admin/repos/repo_add_base.html:38 @@ -1719,12 +1714,8 @@ msgstr "Typ" #: rhodecode/templates/admin/repos/repo_edit.html:89 #: rhodecode/templates/forks/fork.html:72 #: rhodecode/templates/settings/repo_settings.html:80 -msgid "" -"Private repositories are only visible to people explicitly added as " -"collaborators." -msgstr "" -"Prywatne repozytoria są widoczne tylko dla osób bezpośrednio dodanych " -"jako współpracownicy." +msgid "Private repositories are only visible to people explicitly added as collaborators." +msgstr "Prywatne repozytoria są widoczne tylko dla osób bezpośrednio dodanych jako współpracownicy." #: rhodecode/templates/admin/defaults/defaults.html:55 #: rhodecode/templates/admin/repos/repo_edit.html:94 @@ -1900,14 +1891,8 @@ msgid "Anonymous access" msgstr "Dostęp anonimowy" #: rhodecode/templates/admin/permissions/permissions.html:49 -msgid "" -"All default permissions on each repository will be reset to choosen " -"permission, note that all custom default permission on repositories will " -"be lost" -msgstr "" -"Wszystkie uprawnienia domyślne każdego repozytorium zostaną przywrócone. " -"Wybrane uprawnienie zostaną skasowane. Pamiętaj, że wszystkie " -"niestandardowe uprawnienia w repozytoriach zostaną utracone." +msgid "All default permissions on each repository will be reset to choosen permission, note that all custom default permission on repositories will be lost" +msgstr "Wszystkie uprawnienia domyślne każdego repozytorium zostaną przywrócone. Wybrane uprawnienie zostaną skasowane. Pamiętaj, że wszystkie niestandardowe uprawnienia w repozytoriach zostaną utracone." #: rhodecode/templates/admin/permissions/permissions.html:50 #: rhodecode/templates/admin/permissions/permissions.html:63 @@ -1924,14 +1909,8 @@ msgid "Repository group" msgstr "Repozytorium grupy" #: rhodecode/templates/admin/permissions/permissions.html:62 -msgid "" -"All default permissions on each repository group will be reset to choosen" -" permission, note that all custom default permission on repositories " -"group will be lost" -msgstr "" -"Wszystkie uprawnienia domyślne każdego repozytorium zostaną przywrócone. " -"Wybrane uprawnienie zostaną skasowane. Pamiętaj, że wszystkie " -"niestandardowe uprawnienia w repozytoriach zostaną utracone." +msgid "All default permissions on each repository group will be reset to choosen permission, note that all custom default permission on repositories group will be lost" +msgstr "Wszystkie uprawnienia domyślne każdego repozytorium zostaną przywrócone. Wybrane uprawnienie zostaną skasowane. Pamiętaj, że wszystkie niestandardowe uprawnienia w repozytoriach zostaną utracone." #: rhodecode/templates/admin/permissions/permissions.html:69 msgid "Registration" @@ -2112,12 +2091,8 @@ msgid "Confirm to invalidate repository msgstr "Potwierdź unieważnienie pamięci podręcznej repozytorium" #: rhodecode/templates/admin/repos/repo_edit.html:193 -msgid "" -"Manually invalidate cache for this repository. On first access repository" -" will be cached again" -msgstr "" -"Ręcznie unieważnienie cache dla tego repozytorium. Przy pierwszym " -"dostępie do repozytorium zostanie dodany do bufora ponownie" +msgid "Manually invalidate cache for this repository. On first access repository will be cached again" +msgstr "Ręcznie unieważnienie cache dla tego repozytorium. Przy pierwszym dostępie do repozytorium zostanie dodany do bufora ponownie" #: rhodecode/templates/admin/repos/repo_edit.html:198 msgid "List of cached values" @@ -2125,12 +2100,11 @@ msgstr "Lista buforowanych wartości" #: rhodecode/templates/admin/repos/repo_edit.html:201 msgid "Prefix" -msgstr "" +msgstr "Prefiks" #: rhodecode/templates/admin/repos/repo_edit.html:202 -#, fuzzy msgid "Key" -msgstr "Klucz API" +msgstr "Klucz" #: rhodecode/templates/admin/repos/repo_edit.html:203 #: rhodecode/templates/admin/users/user_add.html:86 @@ -2156,12 +2130,8 @@ msgid "Add to public journal" msgstr "Dodaj do dziennika publicznego" #: rhodecode/templates/admin/repos/repo_edit.html:231 -msgid "" -"All actions made on this repository will be accessible to everyone in " -"public journal" -msgstr "" -"Wszystkie działania wykonywane na tym repozytorium będą dostępne dla " -"wszystkich w dzienniku publicznym" +msgid "All actions made on this repository will be accessible to everyone in public journal" +msgstr "Wszystkie działania wykonywane na tym repozytorium będą dostępne dla wszystkich w dzienniku publicznym" #: rhodecode/templates/admin/repos/repo_edit.html:238 msgid "Locking" @@ -2189,9 +2159,7 @@ msgstr "Repozytorium nie jest zablokowan #: rhodecode/templates/admin/repos/repo_edit.html:252 msgid "Force locking on repository. Works only when anonymous access is disabled" -msgstr "" -"Wymuś blokowanie na repozytorium. Działa tylko wtedy, gdy dostęp " -"anonimowy jest wyłączony" +msgstr "Wymuś blokowanie na repozytorium. Działa tylko wtedy, gdy dostęp anonimowy jest wyłączony" #: rhodecode/templates/admin/repos/repo_edit.html:259 msgid "Set as fork of" @@ -2218,15 +2186,8 @@ msgstr "Potwierdź, aby usunąć repozytorium" #: rhodecode/templates/admin/repos/repo_edit.html:282 #: rhodecode/templates/settings/repo_settings.html:119 -#, fuzzy -msgid "" -"This repository will be renamed in a special way in order to be " -"unaccesible for RhodeCode and VCS systems. If you need fully delete it " -"from file system please do it manually" -msgstr "" -"To repozytorium zostanie zmienione w sposób szczególny, żeby było " -"niedostępne dla strony i systemów VCS. Jeśli chcesz całkowicie usunąć go " -"z systemu plików prosimy zrobić to ręcznie" +msgid "This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. If you need fully delete it from file system please do it manually" +msgstr "To repozytorium zostanie zmienione w sposób szczególny, żeby było niedostępne dla strony i systemów VCS. Jeśli chcesz całkowicie usunąć go z systemu plików prosimy zrobić to ręcznie" #: rhodecode/templates/admin/repos/repo_edit_perms.html:3 #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:3 @@ -2295,14 +2256,14 @@ msgid "Repositories administration" msgstr "Administracja repozytoriami" #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:73 +#, fuzzy msgid "apply to children" -msgstr "" +msgstr "dotyczy dzieci" #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:74 -msgid "" -"Set or revoke permission to all children of that group, including " -"repositories and other groups" -msgstr "" +#, fuzzy +msgid "Set or revoke permission to all children of that group, including repositories and other groups" +msgstr "Ustawia lub cofa uprawnienia do wszystkich dzieci z tej grupy, w tym repozytoria oraz innych grup" #: rhodecode/templates/admin/repos_groups/repos_groups.html:9 #: rhodecode/templates/base/base.html:122 @@ -2320,7 +2281,8 @@ msgstr "" #: rhodecode/templates/files/files_add.html:15 #: rhodecode/templates/files/files_edit.html:15 #: rhodecode/templates/followers/followers.html:9 -#: rhodecode/templates/forks/fork.html:9 rhodecode/templates/forks/forks.html:9 +#: rhodecode/templates/forks/fork.html:9 +#: rhodecode/templates/forks/forks.html:9 #: rhodecode/templates/pullrequests/pullrequest.html:8 #: rhodecode/templates/pullrequests/pullrequest_show.html:8 #: rhodecode/templates/pullrequests/pullrequest_show_all.html:8 @@ -2370,12 +2332,8 @@ msgid "edit repos group" msgstr "edytuj grupy repo" #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:70 -msgid "" -"Enable lock-by-pulling on group. This option will be applied to all other" -" groups and repositories inside" -msgstr "" -"Włącz blokowanie pulling przez grupy. Opcja ta będzie stosowana do " -"wszystkich innych grup i repozytoriów wewnątrz" +msgid "Enable lock-by-pulling on group. This option will be applied to all other groups and repositories inside" +msgstr "Włącz blokowanie pulling przez grupy. Opcja ta będzie stosowana do wszystkich innych grup i repozytoriów wewnątrz" #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:5 msgid "Repositories groups administration" @@ -2404,12 +2362,12 @@ msgid "delete" msgstr "usuń" #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:55 -#, fuzzy, python-format +#, python-format msgid "Confirm to delete this group: %s with %s repository" msgid_plural "Confirm to delete this group: %s with %s repositories" -msgstr[0] "Potwierdz aby usunąć grupę %s wraz z %s repozytorium" -msgstr[1] "Potwierdz aby usunąć grupę %s wraz z %s repozytoriami" -msgstr[2] "Potwierdz aby usunąć grupę %s wraz z %s repozytoriami" +msgstr[0] "Potwierdź żeby usunąć grupę %s wraz z %s repozytorium" +msgstr[1] "Potwierdź żeby usunąć grupę %s wraz z %s repozytoriami" +msgstr[2] "Potwierdź żeby usunąć grupę %s wraz z %s repozytoriami" #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:63 msgid "There are no repositories groups yet" @@ -2451,26 +2409,16 @@ msgid "rescan option" msgstr "ponowne skanowanie opcji" #: rhodecode/templates/admin/settings/settings.html:38 -msgid "" -"In case a repository was deleted from filesystem and there are leftovers " -"in the database check this option to scan obsolete data in database and " -"remove it." -msgstr "" -"W przypadku repozytoriów zostaną usunięte systemy plików i jeśli są " -"pozostałości w bazie danych to ta opcja sprawdzi ją oraz przeskanuje, a " -"następnie usunie je z bazy danych." +msgid "In case a repository was deleted from filesystem and there are leftovers in the database check this option to scan obsolete data in database and remove it." +msgstr "W przypadku repozytoriów zostaną usunięte systemy plików i jeśli są pozostałości w bazie danych to ta opcja sprawdzi ją oraz przeskanuje, a następnie usunie je z bazy danych." #: rhodecode/templates/admin/settings/settings.html:39 msgid "destroy old data" msgstr "zniszcz stare dane" #: rhodecode/templates/admin/settings/settings.html:41 -msgid "" -"Rescan repositories location for new repositories. Also deletes obsolete " -"if `destroy` flag is checked " -msgstr "" -"Skanowanie ponowne lokalizacji dla nowych repozytoriów. Usuwa również " -"nieaktualne jeśli została zaznaczona flaga `zniszcz` do sprawdzana" +msgid "Rescan repositories location for new repositories. Also deletes obsolete if `destroy` flag is checked " +msgstr "Skanowanie ponowne lokalizacji dla nowych repozytoriów. Usuwa również nieaktualne jeśli została zaznaczona flaga `zniszcz` do sprawdzana" #: rhodecode/templates/admin/settings/settings.html:46 msgid "Rescan repositories" @@ -2519,9 +2467,8 @@ msgid "Visualisation settings" msgstr "Ustawienia wizualizacji" #: rhodecode/templates/admin/settings/settings.html:127 -#, fuzzy msgid "General" -msgstr "włącz" +msgstr "Główne" #: rhodecode/templates/admin/settings/settings.html:132 msgid "Use lightweight dashboard" @@ -2560,12 +2507,8 @@ msgid "require ssl for vcs operations" msgstr "wymagaj ssl dla operacji vcs" #: rhodecode/templates/admin/settings/settings.html:203 -msgid "" -"RhodeCode will require SSL for pushing or pulling. If SSL is missing it " -"will return HTTP Error 406: Not Acceptable" -msgstr "" -"RhodeCode wymaga SSL do wysłania zmian lub pobierania. Jeśli brakuje SSL " -"zwróci błąd HTTP 406: Not Acceptable" +msgid "RhodeCode will require SSL for pushing or pulling. If SSL is missing it will return HTTP Error 406: Not Acceptable" +msgstr "RhodeCode wymaga SSL do wysłania zmian lub pobierania. Jeśli brakuje SSL zwróci błąd HTTP 406: Not Acceptable" #: rhodecode/templates/admin/settings/settings.html:209 msgid "Hooks" @@ -2604,26 +2547,16 @@ msgid "hgsubversion extensions" msgstr "rozszerzenia hgsubversion" #: rhodecode/templates/admin/settings/settings.html:246 -msgid "" -"Requires hgsubversion library installed. Allows clonning from svn remote " -"locations" -msgstr "" -"Wymaga biblioteki hgsubversion zainstalowanej. Umożliwia klonowanie z " -"zdalnych lokalizacji svn" +msgid "Requires hgsubversion library installed. Allows clonning from svn remote locations" +msgstr "Wymaga biblioteki hgsubversion zainstalowanej. Umożliwia klonowanie z zdalnych lokalizacji svn" #: rhodecode/templates/admin/settings/settings.html:256 msgid "Repositories location" msgstr "Położenie repozytorium" #: rhodecode/templates/admin/settings/settings.html:261 -msgid "" -"This a crucial application setting. If you are really sure you need to " -"change this, you must restart application in order to make this setting " -"take effect. Click this label to unlock." -msgstr "" -"To kluczowe ustawienia aplikacji. Jeśli jesteś pewny, że chcesz to " -"zmienić, należy ponownie uruchomić aplikację w celu zaktualizowania " -"lokalizacji. Kliknij tą etykietę, żeby odblokować." +msgid "This a crucial application setting. If you are really sure you need to change this, you must restart application in order to make this setting take effect. Click this label to unlock." +msgstr "To kluczowe ustawienia aplikacji. Jeśli jesteś pewny, że chcesz to zmienić, należy ponownie uruchomić aplikację w celu zaktualizowania lokalizacji. Kliknij tą etykietę, żeby odblokować." #: rhodecode/templates/admin/settings/settings.html:262 #: rhodecode/templates/base/base.html:221 @@ -2631,12 +2564,8 @@ msgid "unlock" msgstr "odblokowany" #: rhodecode/templates/admin/settings/settings.html:263 -msgid "" -"Location where repositories are stored. After changing this value a " -"restart, and rescan is required" -msgstr "" -"Miejsce, w którym przechowywane są repozytoria. Po zmianie tej wartości " -"jest wymagany restart i ponowne skanowanie" +msgid "Location where repositories are stored. After changing this value a restart, and rescan is required" +msgstr "Miejsce, w którym przechowywane są repozytoria. Po zmianie tej wartości jest wymagany restart i ponowne skanowanie" #: rhodecode/templates/admin/settings/settings.html:283 msgid "Test Email" @@ -2716,12 +2645,8 @@ msgstr "Dziedziczą uprawnienia domyślne" #: rhodecode/templates/admin/users/user_edit.html:156 #: rhodecode/templates/admin/users_groups/users_group_edit.html:113 #, python-format -msgid "" -"Select to inherit permissions from %s settings. With this selected below " -"options does not have any action" -msgstr "" -"Zaznacz, żeby dziedziczyć uprawnienia z %s ustawień. Po wybraniu tej " -"opcji, poniżej nie ma żadnych działań" +msgid "Select to inherit permissions from %s settings. With this selected below options does not have any action" +msgstr "Zaznacz, żeby dziedziczyć uprawnienia z %s ustawień. Po wybraniu tej opcji, poniżej nie ma żadnych działań" #: rhodecode/templates/admin/users/user_edit.html:162 #: rhodecode/templates/admin/users_groups/users_group_edit.html:119 @@ -3010,7 +2935,8 @@ msgid "Products" msgstr "Produkty" #: rhodecode/templates/base/base.html:152 -#: rhodecode/templates/base/base.html:182 rhodecode/templates/base/root.html:47 +#: rhodecode/templates/base/base.html:182 +#: rhodecode/templates/base/root.html:47 msgid "loading..." msgstr "wczytywanie..." @@ -3064,7 +2990,8 @@ msgstr "ustawienia repozytorium" msgid "fork" msgstr "gałąż" -#: rhodecode/templates/base/base.html:212 rhodecode/templates/base/root.html:50 +#: rhodecode/templates/base/base.html:212 +#: rhodecode/templates/base/root.html:50 #: rhodecode/templates/changelog/changelog.html:43 msgid "Open new pull request" msgstr "Otwórz nową prośbę o połączenie gałęzi" @@ -3095,7 +3022,6 @@ msgid "permissions" msgstr "uprawnienia" #: rhodecode/templates/base/base.html:239 -#, fuzzy msgid "defaults" msgstr "domyślne" @@ -3215,9 +3141,8 @@ msgid "compare fork with %s" msgstr "porównaj gałęzie %s" #: rhodecode/templates/changelog/changelog.html:40 -#, fuzzy msgid "Compare fork with parent" -msgstr "porównaj fork w rodzicem" +msgstr "porównaj gałąź w rodzicem" #: rhodecode/templates/changelog/changelog.html:49 msgid "Show" @@ -3319,8 +3244,9 @@ msgid "Changeset" msgstr "Grupy zmian" #: rhodecode/templates/changeset/changeset.html:52 +#, fuzzy msgid "No children" -msgstr "" +msgstr "Brak dzieci" #: rhodecode/templates/changeset/changeset.html:70 #: rhodecode/templates/changeset/diff_block.html:20 @@ -3357,22 +3283,22 @@ msgstr[2] "(%d linii)" #: rhodecode/templates/changeset/changeset.html:122 #: rhodecode/templates/compare/compare_diff.html:44 #: rhodecode/templates/pullrequests/pullrequest_show.html:76 -#, fuzzy, python-format +#, python-format msgid "%s file changed" msgid_plural "%s files changed" -msgstr[0] "%s plik zmieniony" -msgstr[1] "%s plików zmienionych" -msgstr[2] "%s plików zmienionych" +msgstr[0] "%s plik został zmieniony" +msgstr[1] "%s pliki zostały zmienione" +msgstr[2] "%s plików zostało zmienionych" #: rhodecode/templates/changeset/changeset.html:124 #: rhodecode/templates/compare/compare_diff.html:46 #: rhodecode/templates/pullrequests/pullrequest_show.html:78 -#, fuzzy, python-format +#, python-format msgid "%s file changed with %s insertions and %s deletions" msgid_plural "%s files changed with %s insertions and %s deletions" -msgstr[0] "%s plik zmieniony z %s inserjcami i %s usunieciami" -msgstr[1] "%s plików zmienionych z %s inserjcami i %s usunieciami" -msgstr[2] "%s plików zmienionych z %s inserjcami i %s usunieciami" +msgstr[0] "%s plik został zmieniony z %s inercjami i %s usunięciami" +msgstr[1] "%s plików zostało zmienionych z %s inercjami i %s usunięciami" +msgstr[2] "%s plików zostało zmienionych z %s inercjami i %s usunięciami" #: rhodecode/templates/changeset/changeset_file_comment.html:42 msgid "Submitting..." @@ -3391,9 +3317,7 @@ msgstr "Komentarze analizowane za pomocą %s składni od %s wsparcia." #: rhodecode/templates/changeset/changeset_file_comment.html:48 #: rhodecode/templates/changeset/changeset_file_comment.html:123 msgid "Use @username inside this text to send notification to this RhodeCode user" -msgstr "" -"Użyj @username wewnątrz tego tekstu, aby wysłać powiadomienie do " -"użytkownika strony" +msgstr "Użyj @username wewnątrz tego tekstu, aby wysłać powiadomienie do użytkownika strony" #: rhodecode/templates/changeset/changeset_file_comment.html:59 #: rhodecode/templates/changeset/changeset_file_comment.html:143 @@ -3440,9 +3364,8 @@ msgid "Compare View" msgstr "Wyświetl Porównanie" #: rhodecode/templates/changeset/changeset_range.html:29 -#, fuzzy msgid "Show combined compare" -msgstr "pokaż online komentarz" +msgstr "Pokaż połączone porównaj" #: rhodecode/templates/changeset/changeset_range.html:54 msgid "Files affected" @@ -3462,18 +3385,17 @@ msgstr "Brak zestawienia zmian" #: rhodecode/templates/compare/compare_diff.html:37 #: rhodecode/templates/pullrequests/pullrequest_show.html:69 -#, fuzzy, python-format +#, python-format msgid "Showing %s commit" msgid_plural "Showing %s commits" -msgstr[0] "Wyswietlane %s commit" -msgstr[1] "Wyswietlane %s commits" -msgstr[2] "Wyswietlane %s commits" +msgstr[0] "Pokaż %s komentarz" +msgstr[1] "Pokaż %s komentarze" +msgstr[2] "Pokaż %s komentarze" #: rhodecode/templates/compare/compare_diff.html:52 #: rhodecode/templates/pullrequests/pullrequest_show.html:84 -#, fuzzy msgid "No files" -msgstr "pliki" +msgstr "Brak plików" #: rhodecode/templates/data_table/_dt_elements.html:39 #: rhodecode/templates/data_table/_dt_elements.html:41 @@ -3527,42 +3449,37 @@ msgid "Confirm to delete this user: %s" msgstr "Potwierdź usunięcie tego użytkownika: %s" #: rhodecode/templates/email_templates/changeset_comment.html:10 -#, fuzzy msgid "New status$" -msgstr "zmień status" +msgstr "Nowy status$" #: rhodecode/templates/email_templates/main.html:8 -#, fuzzy msgid "This is a notification from RhodeCode." msgstr "To jest powiadomienie z strony" #: rhodecode/templates/email_templates/password_reset.html:4 msgid "Hello" -msgstr "" +msgstr "Witaj" #: rhodecode/templates/email_templates/password_reset.html:6 msgid "We received a request to create a new password for your account." -msgstr "" +msgstr "Otrzymaliśmy prośbę o utworzenie nowego hasła do twojego konta." #: rhodecode/templates/email_templates/password_reset.html:8 msgid "You can generate it by clicking following URL" -msgstr "" +msgstr "Możesz wygenerować nowe hasło klikając w link URL poniżej:" #: rhodecode/templates/email_templates/password_reset.html:12 msgid "If you didn't request new password please ignore this email." -msgstr "" +msgstr "Jeśli nie chcesz wygenerować nowego hasła to zignoruj tą wiadomość." #: rhodecode/templates/email_templates/pull_request.html:4 #, python-format -msgid "" -"User %s opened pull request for repository %s and wants you to review " -"changes." -msgstr "" +msgid "User %s opened pull request for repository %s and wants you to review changes." +msgstr "Użytkownik %s zgłosił wniosek połączenia w repozytorium %s i chce żeby sprawdzić zmiany." #: rhodecode/templates/email_templates/pull_request.html:5 -#, fuzzy msgid "title" -msgstr "Tytuł" +msgstr "tytuł" #: rhodecode/templates/email_templates/pull_request.html:6 #: rhodecode/templates/pullrequests/pullrequest.html:115 @@ -3571,37 +3488,32 @@ msgstr "opis" #: rhodecode/templates/email_templates/pull_request.html:11 msgid "revisions for reviewing" -msgstr "" +msgstr "korekty dotyczące rewizji" #: rhodecode/templates/email_templates/pull_request.html:18 -#, fuzzy msgid "View this pull request here" -msgstr "Pokarz wszystkie zmiany" +msgstr "Wyświetl prośby pobrania tutaj" #: rhodecode/templates/email_templates/pull_request_comment.html:4 -#, fuzzy, python-format +#, python-format msgid "User %s commented on pull request #%s for repository %s" -msgstr "" -"Użytkownik %s skomentował wniosek o połączenie gałęzi #%s dla " -"repozytorium %s" +msgstr "Użytkownik %s skomentował wniosek o połączenie gałęzi #%s dla repozytorium %s" #: rhodecode/templates/email_templates/pull_request_comment.html:10 -#, fuzzy msgid "New status" -msgstr "zmień status" +msgstr "Nowy status" #: rhodecode/templates/email_templates/pull_request_comment.html:14 msgid "View this comment here" -msgstr "" +msgstr "Zobacz ten komentarz tutaj" #: rhodecode/templates/email_templates/registration.html:4 -#, fuzzy msgid "A new user have registered in RhodeCode" -msgstr "Udało Ci się zarejestrować na stronie" +msgstr "Nowy użytkownik został zarejestrowany na stronie" #: rhodecode/templates/email_templates/registration.html:9 msgid "View this user here" -msgstr "" +msgstr "Zobacz tego użytkownika tutaj" #: rhodecode/templates/errors/error_document.html:46 #, python-format @@ -3771,9 +3683,8 @@ msgid "show at revision" msgstr "wskaż zmiany" #: rhodecode/templates/files/files_history_box.html:11 -#, fuzzy msgid "show full history" -msgstr "Wczytywanie listy plików..." +msgstr "pokaż pełną historię" #: rhodecode/templates/files/files_history_box.html:16 #, python-format @@ -3784,9 +3695,8 @@ msgstr[1] "%s autorzy" msgstr[2] "%s autorzy" #: rhodecode/templates/files/files_source.html:6 -#, fuzzy msgid "Load file history" -msgstr "Wczytywanie listy plików..." +msgstr "Załaduj historię pliku" #: rhodecode/templates/files/files_source.html:21 msgid "show source" @@ -4020,9 +3930,8 @@ msgid "Compare view" msgstr "Wyświetl porównanie" #: rhodecode/templates/pullrequests/pullrequest_show.html:112 -#, fuzzy msgid "reviewer" -msgstr "%d recenzent" +msgstr "recenzent" #: rhodecode/templates/pullrequests/pullrequest_show_all.html:4 msgid "all pull requests" @@ -4089,14 +3998,12 @@ msgid "%s Settings" msgstr "Ustawienia %s" #: rhodecode/templates/settings/repo_settings.html:102 -#, fuzzy msgid "Delete repository" -msgstr "[skasowane] repozytorium" +msgstr "Usuń repozytorium" #: rhodecode/templates/settings/repo_settings.html:109 -#, fuzzy msgid "Remove repo" -msgstr "usuń" +msgstr "Usuń repo" #: rhodecode/templates/shortlog/shortlog.html:5 #, python-format @@ -4296,9 +4203,8 @@ msgid "%s Tags" msgstr "Etykiety pliku %s" #: rhodecode/templates/tags/tags.html:29 -#, fuzzy msgid "Compare tags" -msgstr "porównanie" +msgstr "Porównaj tagi" #~ msgid "" #~ "%s repository is not mapped to db" @@ -4325,4 +4231,3 @@ msgstr "porównanie" #~ "zmienione w systemie plików proszę " #~ "uruchomić aplikację ponownie, aby ponownie " #~ "przeskanować repozytoria" - diff --git a/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.mo b/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.mo index 872afff120e5d9882adee9f3d0c662bdb3b0788f..11819a8a859aebc396c5278c4afa840673301490 GIT binary patch literal 57171 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 diff --git a/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.po b/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.po --- a/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.po +++ b/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.po @@ -8,15 +8,16 @@ msgid "" msgstr "" "Project-Id-Version: RhodeCode 1.4.4\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2012-12-14 04:19+0100\n" -"PO-Revision-Date: 2012-11-26 15:19+0800\n" +"POT-Creation-Date: 2012-12-14 12:53+0800\n" +"PO-Revision-Date: 2012-12-14 12:57+0800\n" "Last-Translator: xpol \n" "Language-Team: mikespook\n" -"Plural-Forms: nplurals=1; plural=0\n" +"Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.6\n" +"X-Generator: Poedit 1.5.4\n" #: rhodecode/controllers/changelog.py:95 msgid "All Branches" @@ -43,8 +44,8 @@ msgstr "状态修改为%s" #: rhodecode/controllers/changeset.py:345 msgid "" -"Changing status on a changeset associated witha closed pull request is " -"not allowed" +"Changing status on a changeset associated witha closed pull request is not " +"allowed" msgstr "不允许修改已关闭拉取请求的修订集状态" #: rhodecode/controllers/compare.py:75 @@ -58,7 +59,8 @@ msgid "Home page" msgstr "主页" #: rhodecode/controllers/error.py:98 -msgid "The request could not be understood by the server due to malformed syntax." +msgid "" +"The request could not be understood by the server due to malformed syntax." msgstr "由于错误的语法,服务器无法对请求进行响应。" #: rhodecode/controllers/error.py:101 @@ -211,8 +213,7 @@ msgstr "密码重置链接已经发送" #: rhodecode/controllers/login.py:184 msgid "" -"Your password reset was successful, new password has been sent to your " -"email" +"Your password reset was successful, new password has been sent to your email" msgstr "密码已经成功重置,新密码已经发送到你的邮箱" #: rhodecode/controllers/pullrequests.py:76 rhodecode/model/scm.py:556 @@ -240,8 +241,9 @@ msgid "Successfully deleted pull request msgstr "成功删除拉取请求" #: rhodecode/controllers/pullrequests.py:452 -msgid "Closing pull request on other statuses than rejected or approved forbidden" -msgstr "" +msgid "" +"Closing pull request on other statuses than rejected or approved forbidden" +msgstr "只能以批准或者驳回的状态关闭拉取请求" #: rhodecode/controllers/search.py:134 msgid "Invalid search query. Try quoting it." @@ -308,14 +310,12 @@ msgid "Statistics are disabled for this msgstr "该版本库统计功能已经禁用" #: rhodecode/controllers/admin/defaults.py:96 -#, fuzzy msgid "Default settings updated successfully" -msgstr "LDAP设置已经成功更新" +msgstr "默认设置已经成功更新" #: rhodecode/controllers/admin/defaults.py:110 -#, fuzzy msgid "error occurred during update of defaults" -msgstr "更新用户%s时发生错误" +msgstr "更新默认设置时发生错误" #: rhodecode/controllers/admin/ldap_settings.py:50 msgid "BASE" @@ -654,11 +654,11 @@ msgstr "无法编辑该用户" #: rhodecode/controllers/admin/users.py:272 msgid "Granted 'repository create' permission to user" -msgstr "已授予用户‘创建版本库’的权限" +msgstr "已授予用户“创建版本库”的权限" #: rhodecode/controllers/admin/users.py:277 msgid "Revoked 'repository create' permission to user" -msgstr "已撤销用户‘创建版本库’的权限" +msgstr "已撤销用户“创建版本库”的权限" #: rhodecode/controllers/admin/users.py:283 msgid "Granted 'repository fork' permission to user" @@ -716,19 +716,19 @@ msgstr "删除用户组时发生错误" #: rhodecode/controllers/admin/users_groups.py:257 msgid "Granted 'repository create' permission to users group" -msgstr "已授予用户组‘创建版本库’的权限" +msgstr "已授予用户组“创建版本库”的权限" #: rhodecode/controllers/admin/users_groups.py:262 msgid "Revoked 'repository create' permission to users group" -msgstr "已撤销用户组‘创建版本库’的权限" +msgstr "已撤销用户组“创建版本库”的权限" #: rhodecode/controllers/admin/users_groups.py:268 msgid "Granted 'repository fork' permission to users group" -msgstr "已授予用户组‘复刻版本库’的权限" +msgstr "已授予用户组“复刻版本库”的权限" #: rhodecode/controllers/admin/users_groups.py:273 msgid "Revoked 'repository fork' permission to users group" -msgstr "已撤销用户组‘复刻版本库’的权限" +msgstr "已撤销用户组“复刻版本库”的权限" #: rhodecode/lib/auth.py:499 msgid "You need to be a registered user to perform this action" @@ -743,7 +743,8 @@ msgid "binary file" msgstr "二进制文件" #: rhodecode/lib/diffs.py:90 -msgid "Changeset was too big and was cut off, use diff menu to display this diff" +msgid "" +"Changeset was too big and was cut off, use diff menu to display this diff" msgstr "修订集因过大而被截断,可查看原始修订集作为替代" #: rhodecode/lib/diffs.py:100 @@ -795,7 +796,8 @@ msgstr "还有" msgid "%s more" msgstr "%s个" -#: rhodecode/lib/helpers.py:617 rhodecode/templates/changelog/changelog.html:51 +#: rhodecode/lib/helpers.py:617 +#: rhodecode/templates/changelog/changelog.html:51 msgid "revisions" msgstr "修订" @@ -899,10 +901,11 @@ msgstr "没有文件" #: rhodecode/lib/helpers.py:1163 #, python-format msgid "" -"%s repository is not mapped to db perhaps it was created or renamed from " -"the filesystem please run the application again in order to rescan " -"repositories" -msgstr "版本库%s没有映射到数据库,可能是从文件系统创建或者重命名,请重启RhodeCode以重新扫描版本库" +"%s repository is not mapped to db perhaps it was created or renamed from the " +"filesystem please run the application again in order to rescan repositories" +msgstr "" +"版本库%s没有映射到数据库,可能是从文件系统创建或者重命名,请重启RhodeCode以重" +"新扫描版本库" #: rhodecode/lib/utils2.py:403 #, python-format @@ -1130,9 +1133,10 @@ msgstr "由于是系统帐号,无法删除该用户" #: rhodecode/model/user.py:333 #, python-format msgid "" -"user \"%s\" still owns %s repositories and cannot be removed. Switch " -"owners or remove those repositories. %s" -msgstr "由于用户 \"%s\" 拥有版本库%s因而无法删除,请修改版本库所有者或删除版本库。%s" +"user \"%s\" still owns %s repositories and cannot be removed. Switch owners " +"or remove those repositories. %s" +msgstr "" +"由于用户 \"%s\" 拥有版本库%s因而无法删除,请修改版本库所有者或删除版本库。%s" #: rhodecode/model/validators.py:36 rhodecode/model/validators.py:37 msgid "Value cannot be an empty list" @@ -1150,9 +1154,10 @@ msgstr "不允许用户名 \"%(username)s\"" #: rhodecode/model/validators.py:87 msgid "" -"Username may only contain alphanumeric characters underscores, periods or" -" dashes and must begin with alphanumeric character" -msgstr "只能使用字母、数字、下划线、小数点或减号作为用户名,且必须由数字或字母开头" +"Username may only contain alphanumeric characters underscores, periods or " +"dashes and must begin with alphanumeric character" +msgstr "" +"只能使用字母、数字、下划线、小数点或减号作为用户名,且必须由数字或字母开头" #: rhodecode/model/validators.py:115 #, python-format @@ -1172,7 +1177,8 @@ msgstr "用户组 \"%(usersgroup)s\" 已经存在" msgid "" "users group name may only contain alphanumeric characters underscores, " "periods or dashes and must begin with alphanumeric character" -msgstr "只能使用字母、数字、下划线、小数点或减号作为用户组名,且必须由数字或字母开头" +msgstr "" +"只能使用字母、数字、下划线、小数点或减号作为用户组名,且必须由数字或字母开头" #: rhodecode/model/validators.py:175 msgid "Cannot assign this group as parent" @@ -1263,8 +1269,8 @@ msgstr "邮件地址\"%(email)s\"不存在" #: rhodecode/model/validators.py:663 msgid "" -"The LDAP Login attribute of the CN must be specified - this is the name " -"of the attribute that is equivalent to \"username\"" +"The LDAP Login attribute of the CN must be specified - this is the name of " +"the attribute that is equivalent to \"username\"" msgstr "LDAP 登陆属性的 CN 必须指定 - 这个名字作为用户名" #: rhodecode/model/validators.py:682 @@ -1606,22 +1612,20 @@ msgid "Admin journal" msgstr "系统日志" #: rhodecode/templates/admin/admin.html:10 -#, fuzzy msgid "journal filter..." -msgstr "快速过滤..." +msgstr "日志过滤..." #: rhodecode/templates/admin/admin.html:12 #: rhodecode/templates/journal/journal.html:11 -#, fuzzy msgid "filter" -msgstr "文件" +msgstr "过滤" #: rhodecode/templates/admin/admin.html:13 #: rhodecode/templates/journal/journal.html:12 #, python-format msgid "%s entry" msgid_plural "%s entries" -msgstr[0] "" +msgstr[0] "%s条" #: rhodecode/templates/admin/admin_log.html:6 #: rhodecode/templates/admin/repos/repos.html:74 @@ -1657,14 +1661,12 @@ msgstr "无操作" #: rhodecode/templates/admin/defaults/defaults.html:5 #: rhodecode/templates/admin/defaults/defaults.html:25 -#, fuzzy msgid "Repositories defaults" -msgstr "版本库组" +msgstr "版本库默认设置" #: rhodecode/templates/admin/defaults/defaults.html:11 -#, fuzzy msgid "Defaults" -msgstr "默认" +msgstr "默认设置" #: rhodecode/templates/admin/defaults/defaults.html:35 #: rhodecode/templates/admin/repos/repo_add_base.html:38 @@ -1858,9 +1860,10 @@ msgstr "匿名访问" #: rhodecode/templates/admin/permissions/permissions.html:49 msgid "" "All default permissions on each repository will be reset to choosen " -"permission, note that all custom default permission on repositories will " -"be lost" -msgstr "所有版本库的默认权限将被重置到选择的权限,所有版本库的自定义权限将被丢弃" +"permission, note that all custom default permission on repositories will be " +"lost" +msgstr "" +"所有版本库的默认权限将被重置到选择的权限,所有版本库的自定义权限将被丢弃" #: rhodecode/templates/admin/permissions/permissions.html:50 #: rhodecode/templates/admin/permissions/permissions.html:63 @@ -1877,12 +1880,12 @@ msgid "Repository group" msgstr "版本库组" #: rhodecode/templates/admin/permissions/permissions.html:62 -#, fuzzy msgid "" -"All default permissions on each repository group will be reset to choosen" -" permission, note that all custom default permission on repositories " -"group will be lost" -msgstr "所有版本库的默认权限将被重置到选择的权限,所有版本库的自定义权限将被丢弃" +"All default permissions on each repository group will be reset to choosen " +"permission, note that all custom default permission on repositories group " +"will be lost" +msgstr "" +"所有版本库组的默认权限将被重置到选择的权限,所有版本库组的自定义权限将被丢弃" #: rhodecode/templates/admin/permissions/permissions.html:69 msgid "Registration" @@ -1955,7 +1958,8 @@ msgstr "文件浏览、下载、whoosh和README的默认修订版本" #: rhodecode/templates/admin/repos/repo_edit.html:79 #: rhodecode/templates/forks/fork.html:63 #: rhodecode/templates/settings/repo_settings.html:70 -msgid "Keep it short and to the point. Use a README file for longer descriptions." +msgid "" +"Keep it short and to the point. Use a README file for longer descriptions." msgstr "保持简短。用README文件来写更长的描述。" #: rhodecode/templates/admin/repos/repo_add_base.html:73 @@ -2064,8 +2068,8 @@ msgstr "确认清除版本库缓存" #: rhodecode/templates/admin/repos/repo_edit.html:193 msgid "" -"Manually invalidate cache for this repository. On first access repository" -" will be cached again" +"Manually invalidate cache for this repository. On first access repository " +"will be cached again" msgstr "手动清除版本库缓存。之后第一次访问的时候将重建缓存" #: rhodecode/templates/admin/repos/repo_edit.html:198 @@ -2105,8 +2109,8 @@ msgstr "添加到公共日志" #: rhodecode/templates/admin/repos/repo_edit.html:231 msgid "" -"All actions made on this repository will be accessible to everyone in " -"public journal" +"All actions made on this repository will be accessible to everyone in public " +"journal" msgstr "任何人都可以在公共日志上看到这个版本库上的所有动作" #: rhodecode/templates/admin/repos/repo_edit.html:238 @@ -2134,7 +2138,8 @@ msgid "Repository is not locked" msgstr "版本库未锁定" #: rhodecode/templates/admin/repos/repo_edit.html:252 -msgid "Force locking on repository. Works only when anonymous access is disabled" +msgid "" +"Force locking on repository. Works only when anonymous access is disabled" msgstr "强制锁定版本库。只有在禁止匿名访问时候才有效" #: rhodecode/templates/admin/repos/repo_edit.html:259 @@ -2162,14 +2167,13 @@ msgstr "确认删除版本库" #: rhodecode/templates/admin/repos/repo_edit.html:282 #: rhodecode/templates/settings/repo_settings.html:119 -#, fuzzy msgid "" -"This repository will be renamed in a special way in order to be " -"unaccesible for RhodeCode and VCS systems. If you need fully delete it " -"from file system please do it manually" +"This repository will be renamed in a special way in order to be unaccesible " +"for RhodeCode and VCS systems. If you need fully delete it from file system " +"please do it manually" msgstr "" -"这个版本库将以特殊的方式重命名这样RhodeCode和版本控制系统将不能访问它。\n" -" 如果需要从文件系统完全删除,你需要手动操作" +"这个版本库将以特殊的方式重命名这样RhodeCode和版本控制系统将不能访问它。如果需" +"要从文件系统完全删除,请要手动操作" #: rhodecode/templates/admin/repos/repo_edit_perms.html:3 #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:3 @@ -2263,7 +2267,8 @@ msgstr "设置或者撤销该组所有成员的权限,包括版本库和其他组" #: rhodecode/templates/files/files_add.html:15 #: rhodecode/templates/files/files_edit.html:15 #: rhodecode/templates/followers/followers.html:9 -#: rhodecode/templates/forks/fork.html:9 rhodecode/templates/forks/forks.html:9 +#: rhodecode/templates/forks/fork.html:9 +#: rhodecode/templates/forks/forks.html:9 #: rhodecode/templates/pullrequests/pullrequest.html:8 #: rhodecode/templates/pullrequests/pullrequest_show.html:8 #: rhodecode/templates/pullrequests/pullrequest_show_all.html:8 @@ -2314,8 +2319,8 @@ msgstr "编辑版本库组" #: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:70 msgid "" -"Enable lock-by-pulling on group. This option will be applied to all other" -" groups and repositories inside" +"Enable lock-by-pulling on group. This option will be applied to all other " +"groups and repositories inside" msgstr "启用组的拉取锁定。这个选项将应用到组内的其他组和版本库" #: rhodecode/templates/admin/repos_groups/repos_groups_show.html:5 @@ -2391,10 +2396,11 @@ msgstr "重新扫描选项" #: rhodecode/templates/admin/settings/settings.html:38 msgid "" -"In case a repository was deleted from filesystem and there are leftovers " -"in the database check this option to scan obsolete data in database and " -"remove it." -msgstr "如果版本库已经从文件系统删除,但数据库仍然有遗留信息,请勾选该项进行清理" +"In case a repository was deleted from filesystem and there are leftovers in " +"the database check this option to scan obsolete data in database and remove " +"it." +msgstr "" +"如果版本库已经从文件系统删除,但数据库仍然有遗留信息,请勾选该项进行清理" #: rhodecode/templates/admin/settings/settings.html:39 msgid "destroy old data" @@ -2402,9 +2408,10 @@ msgstr "清理旧数据" #: rhodecode/templates/admin/settings/settings.html:41 msgid "" -"Rescan repositories location for new repositories. Also deletes obsolete " -"if `destroy` flag is checked " -msgstr "重新扫描版本库路径以发现新版本库。 同时删除过时的,如果设置有 `destroy` 标志" +"Rescan repositories location for new repositories. Also deletes obsolete if " +"`destroy` flag is checked " +msgstr "" +"重新扫描版本库路径以发现新版本库。 同时删除过时的,如果设置有 `destroy` 标志" #: rhodecode/templates/admin/settings/settings.html:46 msgid "Rescan repositories" @@ -2494,9 +2501,11 @@ msgstr "要求使用SSL进行版本控制系统操作" #: rhodecode/templates/admin/settings/settings.html:203 msgid "" -"RhodeCode will require SSL for pushing or pulling. If SSL is missing it " -"will return HTTP Error 406: Not Acceptable" -msgstr "勾选后RhodeCode将要求使用SSL进行推送和拉取。如果没有使用SSL将返回HTTP 406错误:Not Acceptable" +"RhodeCode will require SSL for pushing or pulling. If SSL is missing it will " +"return HTTP Error 406: Not Acceptable" +msgstr "" +"勾选后RhodeCode将要求使用SSL进行推送和拉取。如果没有使用SSL将返回HTTP 406错" +"误:Not Acceptable" #: rhodecode/templates/admin/settings/settings.html:209 msgid "Hooks" @@ -2547,8 +2556,8 @@ msgstr "版本库路径" #: rhodecode/templates/admin/settings/settings.html:261 msgid "" "This a crucial application setting. If you are really sure you need to " -"change this, you must restart application in order to make this setting " -"take effect. Click this label to unlock." +"change this, you must restart application in order to make this setting take " +"effect. Click this label to unlock." msgstr "这是一个关键设置。如果确认修改该项设置,请重启服务以便设置生效。" #: rhodecode/templates/admin/settings/settings.html:262 @@ -2558,8 +2567,8 @@ msgstr "解锁" #: rhodecode/templates/admin/settings/settings.html:263 msgid "" -"Location where repositories are stored. After changing this value a " -"restart, and rescan is required" +"Location where repositories are stored. After changing this value a restart, " +"and rescan is required" msgstr "版本库存储路径。 修改后需要重启和重新扫描" #: rhodecode/templates/admin/settings/settings.html:283 @@ -2932,7 +2941,8 @@ msgid "Products" msgstr "产品" #: rhodecode/templates/base/base.html:152 -#: rhodecode/templates/base/base.html:182 rhodecode/templates/base/root.html:47 +#: rhodecode/templates/base/base.html:182 +#: rhodecode/templates/base/root.html:47 msgid "loading..." msgstr "载入中..." @@ -2986,7 +2996,8 @@ msgstr "版本库选项" msgid "fork" msgstr "复刻" -#: rhodecode/templates/base/base.html:212 rhodecode/templates/base/root.html:50 +#: rhodecode/templates/base/base.html:212 +#: rhodecode/templates/base/root.html:50 #: rhodecode/templates/changelog/changelog.html:43 msgid "Open new pull request" msgstr "新建拉取请求" @@ -3017,9 +3028,8 @@ msgid "permissions" msgstr "权限" #: rhodecode/templates/base/base.html:239 -#, fuzzy msgid "defaults" -msgstr "默认" +msgstr "默认设置" #: rhodecode/templates/base/base.html:240 msgid "settings" @@ -3238,9 +3248,8 @@ msgid "Changeset" msgstr "修订集" #: rhodecode/templates/changeset/changeset.html:52 -#, fuzzy msgid "No children" -msgstr "应用到成员" +msgstr "无子对象" #: rhodecode/templates/changeset/changeset.html:70 #: rhodecode/templates/changeset/diff_block.html:20 @@ -3302,7 +3311,8 @@ msgstr "评论使用%s语法并支持%s" #: rhodecode/templates/changeset/changeset_file_comment.html:48 #: rhodecode/templates/changeset/changeset_file_comment.html:123 -msgid "Use @username inside this text to send notification to this RhodeCode user" +msgid "" +"Use @username inside this text to send notification to this RhodeCode user" msgstr "在文本中使用 @用户名 以发送通知到该RhodeCode用户" #: rhodecode/templates/changeset/changeset_file_comment.html:59 @@ -3433,9 +3443,8 @@ msgid "Confirm to delete this user: %s" msgstr "确认删除用户:%s" #: rhodecode/templates/email_templates/changeset_comment.html:10 -#, fuzzy msgid "New status$" -msgstr "改变状态" +msgstr "新状态$" #: rhodecode/templates/email_templates/main.html:8 msgid "This is a notification from RhodeCode." @@ -3443,29 +3452,28 @@ msgstr "这是一个RhodeCode通知。" #: rhodecode/templates/email_templates/password_reset.html:4 msgid "Hello" -msgstr "" +msgstr "你好" #: rhodecode/templates/email_templates/password_reset.html:6 msgid "We received a request to create a new password for your account." -msgstr "" +msgstr "我们收到重置你用户密码的请求。" #: rhodecode/templates/email_templates/password_reset.html:8 msgid "You can generate it by clicking following URL" -msgstr "" +msgstr "点击下面的链接以重新生成密码:" #: rhodecode/templates/email_templates/password_reset.html:12 msgid "If you didn't request new password please ignore this email." -msgstr "" +msgstr "如果你没有要求重置密码,请忽略这封邮件。" #: rhodecode/templates/email_templates/pull_request.html:4 #, python-format msgid "" "User %s opened pull request for repository %s and wants you to review " "changes." -msgstr "" +msgstr "用户%s在版本库%s中创建了一个拉取请求需要你检视" #: rhodecode/templates/email_templates/pull_request.html:5 -#, fuzzy msgid "title" msgstr "标题" @@ -3476,35 +3484,32 @@ msgstr "描述" #: rhodecode/templates/email_templates/pull_request.html:11 msgid "revisions for reviewing" -msgstr "" +msgstr "待检视修订" #: rhodecode/templates/email_templates/pull_request.html:18 -#, fuzzy msgid "View this pull request here" -msgstr "为这个拉取请求增加检视人员" +msgstr "查看拉取请求" #: rhodecode/templates/email_templates/pull_request_comment.html:4 -#, fuzzy, python-format +#, python-format msgid "User %s commented on pull request #%s for repository %s" -msgstr "" +msgstr "用户%s评论了版本库%s的拉取请求%s" #: rhodecode/templates/email_templates/pull_request_comment.html:10 -#, fuzzy msgid "New status" -msgstr "改变状态" +msgstr "新状态" #: rhodecode/templates/email_templates/pull_request_comment.html:14 msgid "View this comment here" -msgstr "" +msgstr "查看评论" #: rhodecode/templates/email_templates/registration.html:4 -#, fuzzy msgid "A new user have registered in RhodeCode" -msgstr "成功注册到RhodeCode" +msgstr "新用户注册RhodeCode" #: rhodecode/templates/email_templates/registration.html:9 msgid "View this user here" -msgstr "" +msgstr "查看用户" #: rhodecode/templates/errors/error_document.html:46 #, python-format @@ -3674,9 +3679,8 @@ msgid "show at revision" msgstr "显示修订" #: rhodecode/templates/files/files_history_box.html:11 -#, fuzzy msgid "show full history" -msgstr "加载文件历史记录..." +msgstr "显示全部历史记录" #: rhodecode/templates/files/files_history_box.html:16 #, python-format @@ -3918,9 +3922,8 @@ msgid "Compare view" msgstr "比较显示" #: rhodecode/templates/pullrequests/pullrequest_show.html:112 -#, fuzzy msgid "reviewer" -msgstr "%d个检视者" +msgstr "检视者" #: rhodecode/templates/pullrequests/pullrequest_show_all.html:4 msgid "all pull requests" @@ -3987,14 +3990,12 @@ msgid "%s Settings" msgstr "%s设置" #: rhodecode/templates/settings/repo_settings.html:102 -#, fuzzy msgid "Delete repository" -msgstr "[删除]版本库" +msgstr "删除版本库" #: rhodecode/templates/settings/repo_settings.html:109 -#, fuzzy msgid "Remove repo" -msgstr "删除" +msgstr "删除版本库" #: rhodecode/templates/shortlog/shortlog.html:5 #, python-format @@ -4196,20 +4197,3 @@ msgstr "%s标签" #: rhodecode/templates/tags/tags.html:29 msgid "Compare tags" msgstr "比较标签" - -#~ msgid "" -#~ "%s repository is not mapped to db" -#~ " perhaps it was created or renamed" -#~ " from the file system please run " -#~ "the application again in order to " -#~ "rescan repositories" -#~ msgstr " 版本库%s没有映射到数据库,可能是从文件系统创建或者重命名,请重启RhodeCode以重新扫描版本库" - -#~ msgid "" -#~ "%s repository is not mapped to db" -#~ " perhaps it was moved or renamed " -#~ "from the filesystem please run the " -#~ "application again in order to rescan " -#~ "repositories" -#~ msgstr "版本库%s没有映射到数据库,可能是从文件系统创建或者重命名,请重启RhodeCode以重新扫描版本库" - diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -45,7 +45,8 @@ from rhodecode.lib.auth_ldap import Auth from rhodecode.model import meta from rhodecode.model.user import UserModel -from rhodecode.model.db import Permission, RhodeCodeSetting, User +from rhodecode.model.db import Permission, RhodeCodeSetting, User, UserIpMap +from rhodecode.lib.caching_query import FromCache log = logging.getLogger(__name__) @@ -269,21 +270,34 @@ def login_container_auth(username): return user -def get_container_username(environ, config): +def get_container_username(environ, config, clean_username=False): + """ + Get's the container_auth username (or email). It tries to get username + from REMOTE_USER if container_auth_enabled is enabled, if that fails + it tries to get username from HTTP_X_FORWARDED_USER if proxypass_auth_enabled + is enabled. clean_username extracts the username from this data if it's + having @ in it. + + :param environ: + :param config: + :param clean_username: + """ username = None if str2bool(config.get('container_auth_enabled', False)): from paste.httpheaders import REMOTE_USER username = REMOTE_USER(environ) + log.debug('extracted REMOTE_USER:%s' % (username)) if not username and str2bool(config.get('proxypass_auth_enabled', False)): username = environ.get('HTTP_X_FORWARDED_USER') + log.debug('extracted HTTP_X_FORWARDED_USER:%s' % (username)) - if username: + if username and clean_username: # Removing realm and domain from username username = username.partition('@')[0] username = username.rpartition('\\')[2] - log.debug('Received username %s from container' % username) + log.debug('Received username %s from container' % username) return username @@ -313,11 +327,12 @@ class AuthUser(object): in """ - def __init__(self, user_id=None, api_key=None, username=None): + def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None): self.user_id = user_id self.api_key = None self.username = username + self.ip_addr = ip_addr self.name = '' self.lastname = '' @@ -380,6 +395,24 @@ class AuthUser(object): def is_admin(self): return self.admin + @property + def ip_allowed(self): + """ + Checks if ip_addr used in constructor is allowed from defined list of + allowed ip_addresses for user + + :returns: boolean, True if ip is in allowed ip range + """ + #check IP + allowed_ips = AuthUser.get_allowed_ips(self.user_id, cache=True) + if check_ip_access(source_ip=self.ip_addr, allowed_ips=allowed_ips): + log.debug('IP:%s is in range of %s' % (self.ip_addr, allowed_ips)) + return True + else: + log.info('Access for IP:%s forbidden, ' + 'not in %s' % (self.ip_addr, allowed_ips)) + return False + def __repr__(self): return "" % (self.user_id, self.username, self.is_authenticated) @@ -406,6 +439,17 @@ class AuthUser(object): api_key = cookie_store.get('api_key') return AuthUser(user_id, api_key, username) + @classmethod + def get_allowed_ips(cls, user_id, cache=False): + _set = set() + user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id) + if cache: + user_ips = user_ips.options(FromCache("sql_cache_short", + "get_user_ips_%s" % user_id)) + for ip in user_ips: + _set.add(ip.ip_addr) + return _set or set(['0.0.0.0/0']) + def set_available_permissions(config): """ @@ -450,6 +494,15 @@ class LoginRequired(object): def __wrapper(self, func, *fargs, **fkwargs): cls = fargs[0] user = cls.rhodecode_user + loc = "%s:%s" % (cls.__class__.__name__, func.__name__) + + #check IP + ip_access_ok = True + if not user.ip_allowed: + from rhodecode.lib import helpers as h + h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr))), + category='warning') + ip_access_ok = False api_access_ok = False if self.api_access: @@ -458,9 +511,9 @@ class LoginRequired(object): api_access_ok = True else: log.debug("API KEY token not valid") - loc = "%s:%s" % (cls.__class__.__name__, func.__name__) + log.debug('Checking if %s is authenticated @ %s' % (user.username, loc)) - if user.is_authenticated or api_access_ok: + if (user.is_authenticated or api_access_ok) and ip_access_ok: reason = 'RegularAuth' if user.is_authenticated else 'APIAuth' log.info('user %s is authenticated and granted access to %s ' 'using %s' % (user.username, loc, reason) @@ -682,12 +735,12 @@ class PermsFunction(object): return False self.user_perms = user.permissions if self.check_permissions(): - log.debug('Permission granted for user: %s @ %s', user, + log.debug('Permission to %s granted for user: %s @ %s', self.repo_name, user, check_Location or 'unspecified location') return True else: - log.debug('Permission denied for user: %s @ %s', user, + log.debug('Permission to %s denied for user: %s @ %s', self.repo_name, user, check_Location or 'unspecified location') return False @@ -821,3 +874,122 @@ class HasPermissionAnyMiddleware(object) ) ) return False + + +#============================================================================== +# SPECIAL VERSION TO HANDLE API AUTH +#============================================================================== +class _BaseApiPerm(object): + def __init__(self, *perms): + self.required_perms = set(perms) + + def __call__(self, check_location='unspecified', user=None, repo_name=None): + cls_name = self.__class__.__name__ + check_scope = 'user:%s, repo:%s' % (user, repo_name) + log.debug('checking cls:%s %s %s @ %s', cls_name, + self.required_perms, check_scope, check_location) + if not user: + log.debug('Empty User passed into arguments') + return False + + ## process user + if not isinstance(user, AuthUser): + user = AuthUser(user.user_id) + + if self.check_permissions(user.permissions, repo_name): + log.debug('Permission to %s granted for user: %s @ %s', repo_name, + user, check_location) + return True + + else: + log.debug('Permission to %s denied for user: %s @ %s', repo_name, + user, check_location) + return False + + def check_permissions(self, perm_defs, repo_name): + """ + implement in child class should return True if permissions are ok, + False otherwise + + :param perm_defs: dict with permission definitions + :param repo_name: repo name + """ + raise NotImplementedError() + + +class HasPermissionAllApi(_BaseApiPerm): + def __call__(self, user, check_location=''): + return super(HasPermissionAllApi, self)\ + .__call__(check_location=check_location, user=user) + + def check_permissions(self, perm_defs, repo): + if self.required_perms.issubset(perm_defs.get('global')): + return True + return False + + +class HasPermissionAnyApi(_BaseApiPerm): + def __call__(self, user, check_location=''): + return super(HasPermissionAnyApi, self)\ + .__call__(check_location=check_location, user=user) + + def check_permissions(self, perm_defs, repo): + if self.required_perms.intersection(perm_defs.get('global')): + return True + return False + + +class HasRepoPermissionAllApi(_BaseApiPerm): + def __call__(self, user, repo_name, check_location=''): + return super(HasRepoPermissionAllApi, self)\ + .__call__(check_location=check_location, user=user, + repo_name=repo_name) + + def check_permissions(self, perm_defs, repo_name): + + try: + self._user_perms = set( + [perm_defs['repositories'][repo_name]] + ) + except KeyError: + log.warning(traceback.format_exc()) + return False + if self.required_perms.issubset(self._user_perms): + return True + return False + + +class HasRepoPermissionAnyApi(_BaseApiPerm): + def __call__(self, user, repo_name, check_location=''): + return super(HasRepoPermissionAnyApi, self)\ + .__call__(check_location=check_location, user=user, + repo_name=repo_name) + + def check_permissions(self, perm_defs, repo_name): + + try: + _user_perms = set( + [perm_defs['repositories'][repo_name]] + ) + except KeyError: + log.warning(traceback.format_exc()) + return False + if self.required_perms.intersection(_user_perms): + return True + return False + + +def check_ip_access(source_ip, allowed_ips=None): + """ + Checks if source_ip is a subnet of any of allowed_ips. + + :param source_ip: + :param allowed_ips: list of allowed ips together with mask + """ + from rhodecode.lib import ipaddr + log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips)) + if isinstance(allowed_ips, (tuple, list, set)): + for ip in allowed_ips: + if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip): + return True + return False diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -37,13 +37,18 @@ def _get_ip_addr(environ): proxy_key2 = 'HTTP_X_FORWARDED_FOR' def_key = 'REMOTE_ADDR' - ip = environ.get(proxy_key2) + ip = environ.get(proxy_key) if ip: return ip - ip = environ.get(proxy_key) - + ip = environ.get(proxy_key2) if ip: + # HTTP_X_FORWARDED_FOR can have mutliple ips inside + # the left-most being the original client, and each successive proxy + # that passed the request adding the IP address where it received the + # request from. + if ',' in ip: + ip = ip.split(',')[0].strip() return ip ip = environ.get(def_key, '0.0.0.0') @@ -101,7 +106,7 @@ class BaseVCSController(object): #authenticate this mercurial request using authfunc self.authenticate = BasicAuth('', authfunc, config.get('auth_ret_code')) - self.ipaddr = '0.0.0.0' + self.ip_addr = '0.0.0.0' def _handle_request(self, environ, start_response): raise NotImplementedError() @@ -136,7 +141,7 @@ class BaseVCSController(object): """ invalidate_cache('get_repo_cached_%s' % repo_name) - def _check_permission(self, action, user, repo_name): + def _check_permission(self, action, user, repo_name, ip_addr=None): """ Checks permissions using action (push/pull) user and repository name @@ -145,6 +150,12 @@ class BaseVCSController(object): :param user: user instance :param repo_name: repository name """ + #check IP + authuser = AuthUser(user_id=user.user_id, ip_addr=ip_addr) + if not authuser.ip_allowed: + return False + else: + log.info('Access for IP:%s allowed' % (ip_addr)) if action == 'push': if not HasPermissionAnyMiddleware('repository.write', 'repository.admin')(user, @@ -235,6 +246,9 @@ class BaseVCSController(object): class BaseController(WSGIController): def __before__(self): + """ + __before__ is called before controller methods and after __call__ + """ c.rhodecode_version = __version__ c.rhodecode_instanceid = config.get('instance_id') c.rhodecode_name = config.get('rhodecode_title') @@ -258,7 +272,6 @@ class BaseController(WSGIController): self.sa = meta.Session self.scm_model = ScmModel(self.sa) - self.ip_addr = '' def __call__(self, environ, start_response): """Invoke the Controller""" @@ -273,7 +286,7 @@ class BaseController(WSGIController): cookie_store = CookieStoreWrapper(session.get('rhodecode_user')) user_id = cookie_store.get('user_id', None) username = get_container_username(environ, config) - auth_user = AuthUser(user_id, api_key, username) + auth_user = AuthUser(user_id, api_key, username, self.ip_addr) request.user = auth_user self.rhodecode_user = c.rhodecode_user = auth_user if not self.rhodecode_user.is_authenticated and \ @@ -311,7 +324,7 @@ class BaseRepoController(BaseController) dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name) c.rhodecode_repo = c.rhodecode_db_repo.scm_instance # update last change according to VCS data - dbr.update_last_change(c.rhodecode_repo.last_change) + dbr.update_changeset_cache(dbr.get_changeset()) if c.rhodecode_repo is None: log.error('%s this repository is present in database but it ' 'cannot be created as an scm instance', c.repo_name) diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -347,6 +347,10 @@ def send_email(recipients, subject, body debug = str2bool(config.get('debug')) smtp_auth = email_config.get('smtp_auth') + if not mail_server: + log.error("SMTP mail server not configured - cannot send mail") + return False + try: m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth, mail_port, ssl, tls, debug=debug) diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -164,8 +164,8 @@ class DbManage(object): def step_0(self): # step 0 is the schema upgrade, and than follow proper upgrades - notify('attempting to do database upgrade to version %s' \ - % __dbversion__) + notify('attempting to do database upgrade from ' + 'version %s to version %s' %(curr_version, __dbversion__)) api.upgrade(db_uri, repository_path, __dbversion__) notify('Schema upgrade completed') @@ -286,6 +286,9 @@ class DbManage(object): 'Please validate and check default permissions ' 'in admin panel') + def step_10(self): + pass + upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1) # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE diff --git a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py @@ -619,7 +619,7 @@ class Repository(Base, BaseModel): hg_ui = ret for ui_ in hg_ui: if ui_.ui_active: - log.debug('settings ui from db[%s]%s:%s', ui_.ui_section, + log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section, ui_.ui_key, ui_.ui_value) baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value) diff --git a/rhodecode/lib/dbmigrate/schema/db_1_3_0.py b/rhodecode/lib/dbmigrate/schema/db_1_3_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_3_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_3_0.py @@ -623,7 +623,7 @@ class Repository(Base, BaseModel): hg_ui = ret for ui_ in hg_ui: if ui_.ui_active: - log.debug('settings ui from db[%s]%s:%s', ui_.ui_section, + log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section, ui_.ui_key, ui_.ui_value) baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value) diff --git a/rhodecode/lib/dbmigrate/schema/db_1_5_0.py b/rhodecode/lib/dbmigrate/schema/db_1_5_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_5_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_5_0.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ - rhodecode.model.db_1_4_0 + rhodecode.model.db_1_5_0 ~~~~~~~~~~~~~~~~~~~~~~~~ - Database Models for RhodeCode <=1.5.X + Database Models for RhodeCode <=1.5.2 :created_on: Apr 08, 2010 :author: marcink @@ -23,6 +23,1812 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -#TODO: replace that will db.py content after 1.6 Release +import os +import logging +import datetime +import traceback +import hashlib +import time +from collections import defaultdict + +from sqlalchemy import * +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import relationship, joinedload, class_mapper, validates +from sqlalchemy.exc import DatabaseError +from beaker.cache import cache_region, region_invalidate +from webob.exc import HTTPNotFound + +from pylons.i18n.translation import lazy_ugettext as _ + +from rhodecode.lib.vcs import get_backend +from rhodecode.lib.vcs.utils.helpers import get_scm +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.utils.lazy import LazyProperty + +from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \ + safe_unicode, remove_suffix, remove_prefix +from rhodecode.lib.compat import json +from rhodecode.lib.caching_query import FromCache + +from rhodecode.model.meta import Base, Session + +URL_SEP = '/' +log = logging.getLogger(__name__) + +#============================================================================== +# BASE CLASSES +#============================================================================== + +_hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest() + + +class BaseModel(object): + """ + Base Model for all classess + """ + + @classmethod + def _get_keys(cls): + """return column names for this model """ + return class_mapper(cls).c.keys() + + def get_dict(self): + """ + return dict with keys and values corresponding + to this model data """ + + d = {} + for k in self._get_keys(): + d[k] = getattr(self, k) + + # also use __json__() if present to get additional fields + _json_attr = getattr(self, '__json__', None) + if _json_attr: + # update with attributes from __json__ + if callable(_json_attr): + _json_attr = _json_attr() + for k, val in _json_attr.iteritems(): + d[k] = val + return d + + def get_appstruct(self): + """return list with keys and values tupples corresponding + to this model data """ + + l = [] + for k in self._get_keys(): + l.append((k, getattr(self, k),)) + return l + + def populate_obj(self, populate_dict): + """populate model with data from given populate_dict""" + + for k in self._get_keys(): + if k in populate_dict: + setattr(self, k, populate_dict[k]) + + @classmethod + def query(cls): + return Session().query(cls) + + @classmethod + def get(cls, id_): + if id_: + return cls.query().get(id_) + + @classmethod + def get_or_404(cls, id_): + try: + id_ = int(id_) + except (TypeError, ValueError): + raise HTTPNotFound + + res = cls.query().get(id_) + if not res: + raise HTTPNotFound + return res + + @classmethod + def getAll(cls): + return cls.query().all() + + @classmethod + def delete(cls, id_): + obj = cls.query().get(id_) + Session().delete(obj) + + def __repr__(self): + if hasattr(self, '__unicode__'): + # python repr needs to return str + return safe_str(self.__unicode__()) + return '' % (self.__class__.__name__) + + +class RhodeCodeSetting(Base, BaseModel): + __tablename__ = 'rhodecode_settings' + __table_args__ = ( + UniqueConstraint('app_settings_name'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + + def __init__(self, k='', v=''): + self.app_settings_name = k + self.app_settings_value = v + + @validates('_app_settings_value') + def validate_settings_value(self, key, val): + assert type(val) == unicode + return val + + @hybrid_property + def app_settings_value(self): + v = self._app_settings_value + if self.app_settings_name in ["ldap_active", + "default_repo_enable_statistics", + "default_repo_enable_locking", + "default_repo_private", + "default_repo_enable_downloads"]: + v = str2bool(v) + return v + + @app_settings_value.setter + def app_settings_value(self, val): + """ + Setter that will always make sure we use unicode in app_settings_value + + :param val: + """ + self._app_settings_value = safe_unicode(val) + + def __unicode__(self): + return u"<%s('%s:%s')>" % ( + self.__class__.__name__, + self.app_settings_name, self.app_settings_value + ) + + @classmethod + def get_by_name(cls, key): + return cls.query()\ + .filter(cls.app_settings_name == key).scalar() + + @classmethod + def get_by_name_or_create(cls, key): + res = cls.get_by_name(key) + if not res: + res = cls(key) + return res + + @classmethod + def get_app_settings(cls, cache=False): + + ret = cls.query() + + if cache: + ret = ret.options(FromCache("sql_cache_short", "get_hg_settings")) + + if not ret: + raise Exception('Could not get application settings !') + settings = {} + for each in ret: + settings['rhodecode_' + each.app_settings_name] = \ + each.app_settings_value + + return settings + + @classmethod + def get_ldap_settings(cls, cache=False): + ret = cls.query()\ + .filter(cls.app_settings_name.startswith('ldap_')).all() + fd = {} + for row in ret: + fd.update({row.app_settings_name: row.app_settings_value}) + + return fd + + @classmethod + def get_default_repo_settings(cls, cache=False, strip_prefix=False): + ret = cls.query()\ + .filter(cls.app_settings_name.startswith('default_')).all() + fd = {} + for row in ret: + key = row.app_settings_name + if strip_prefix: + key = remove_prefix(key, prefix='default_') + fd.update({key: row.app_settings_value}) + + return fd + + +class RhodeCodeUi(Base, BaseModel): + __tablename__ = 'rhodecode_ui' + __table_args__ = ( + UniqueConstraint('ui_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + + HOOK_UPDATE = 'changegroup.update' + HOOK_REPO_SIZE = 'changegroup.repo_size' + HOOK_PUSH = 'changegroup.push_logger' + HOOK_PRE_PUSH = 'prechangegroup.pre_push' + HOOK_PULL = 'outgoing.pull_logger' + HOOK_PRE_PULL = 'preoutgoing.pre_pull' + + ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True) + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.ui_key == key).scalar() + + @classmethod + def get_builtin_hooks(cls): + q = cls.query() + q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, + cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, + cls.HOOK_PULL, cls.HOOK_PRE_PULL])) + return q.all() + + @classmethod + def get_custom_hooks(cls): + q = cls.query() + q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, + cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, + cls.HOOK_PULL, cls.HOOK_PRE_PULL])) + q = q.filter(cls.ui_section == 'hooks') + return q.all() + + @classmethod + def get_repos_location(cls): + return cls.get_by_key('/').ui_value + + @classmethod + def create_or_update_hook(cls, key, val): + new_ui = cls.get_by_key(key) or cls() + new_ui.ui_section = 'hooks' + new_ui.ui_active = True + new_ui.ui_key = key + new_ui.ui_value = val + + Session().add(new_ui) + + def __repr__(self): + return '' % (self.__class__.__name__, self.ui_key, + self.ui_value) + + +class User(Base, BaseModel): + __tablename__ = 'users' + __table_args__ = ( + UniqueConstraint('username'), UniqueConstraint('email'), + Index('u_username_idx', 'username'), + Index('u_email_idx', 'email'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + DEFAULT_USER = 'default' + DEFAULT_PERMISSIONS = [ + 'hg.register.manual_activate', 'hg.create.repository', + 'hg.fork.repository', 'repository.read', 'group.read' + ] + user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=True) + admin = Column("admin", Boolean(), nullable=True, unique=None, default=False) + name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None) + ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) + + user_log = relationship('UserLog') + user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') + + repositories = relationship('Repository') + user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') + followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all') + + repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') + repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all') + + group_member = relationship('UsersGroupMember', cascade='all') + + notifications = relationship('UserNotification', cascade='all') + # notifications assigned to this user + user_created_notifications = relationship('Notification', cascade='all') + # comments created by this user + user_comments = relationship('ChangesetComment', cascade='all') + #extra emails for this user + user_emails = relationship('UserEmailMap', cascade='all') + + @hybrid_property + def email(self): + return self._email + + @email.setter + def email(self, val): + self._email = val.lower() if val else None + + @property + def firstname(self): + # alias for future + return self.name + + @property + def emails(self): + other = UserEmailMap.query().filter(UserEmailMap.user==self).all() + return [self.email] + [x.email for x in other] + + @property + def username_and_name(self): + return '%s (%s %s)' % (self.username, self.firstname, self.lastname) + + @property + def full_name(self): + return '%s %s' % (self.firstname, self.lastname) + + @property + def full_name_or_username(self): + return ('%s %s' % (self.firstname, self.lastname) + if (self.firstname and self.lastname) else self.username) + + @property + def full_contact(self): + return '%s %s <%s>' % (self.firstname, self.lastname, self.email) + + @property + def short_contact(self): + return '%s %s' % (self.firstname, self.lastname) + + @property + def is_admin(self): + return self.admin + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, + self.user_id, self.username) + + @classmethod + def get_by_username(cls, username, case_insensitive=False, cache=False): + if case_insensitive: + q = cls.query().filter(cls.username.ilike(username)) + else: + q = cls.query().filter(cls.username == username) + + if cache: + q = q.options(FromCache( + "sql_cache_short", + "get_user_%s" % _hash_key(username) + ) + ) + return q.scalar() + + @classmethod + def get_by_api_key(cls, api_key, cache=False): + q = cls.query().filter(cls.api_key == api_key) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_api_key_%s" % api_key)) + return q.scalar() + + @classmethod + def get_by_email(cls, email, case_insensitive=False, cache=False): + if case_insensitive: + q = cls.query().filter(cls.email.ilike(email)) + else: + q = cls.query().filter(cls.email == email) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_email_key_%s" % email)) + + ret = q.scalar() + if ret is None: + q = UserEmailMap.query() + # try fetching in alternate email map + if case_insensitive: + q = q.filter(UserEmailMap.email.ilike(email)) + else: + q = q.filter(UserEmailMap.email == email) + q = q.options(joinedload(UserEmailMap.user)) + if cache: + q = q.options(FromCache("sql_cache_short", + "get_email_map_key_%s" % email)) + ret = getattr(q.scalar(), 'user', None) + + return ret + + def update_lastlogin(self): + """Update user lastlogin""" + self.last_login = datetime.datetime.now() + Session().add(self) + log.debug('updated user %s lastlogin' % self.username) + + def get_api_data(self): + """ + Common function for generating user related data for API + """ + user = self + data = dict( + user_id=user.user_id, + username=user.username, + firstname=user.name, + lastname=user.lastname, + email=user.email, + emails=user.emails, + api_key=user.api_key, + active=user.active, + admin=user.admin, + ldap_dn=user.ldap_dn, + last_login=user.last_login, + ) + return data + + def __json__(self): + data = dict( + full_name=self.full_name, + full_name_or_username=self.full_name_or_username, + short_contact=self.short_contact, + full_contact=self.full_contact + ) + data.update(self.get_api_data()) + return data + + +class UserEmailMap(Base, BaseModel): + __tablename__ = 'user_email_map' + __table_args__ = ( + Index('uem_email_idx', 'email'), + UniqueConstraint('email'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + __mapper_args__ = {} + + email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) + user = relationship('User', lazy='joined') + + @validates('_email') + def validate_email(self, key, email): + # check if this email is not main one + main_email = Session().query(User).filter(User.email == email).scalar() + if main_email is not None: + raise AttributeError('email %s is present is user table' % email) + return email + + @hybrid_property + def email(self): + return self._email + + @email.setter + def email(self, val): + self._email = val.lower() if val else None + + +class UserLog(Base, BaseModel): + __tablename__ = 'user_logs' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True) + repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None) + + @property + def action_as_day(self): + return datetime.date(*self.action_date.timetuple()[:3]) + + user = relationship('User') + repository = relationship('Repository', cascade='') + + +class UsersGroup(Base, BaseModel): + __tablename__ = 'users_groups' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + + users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None) + inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) + + members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined") + users_group_to_perm = relationship('UsersGroupToPerm', cascade='all') + users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all') + + def __unicode__(self): + return u'' % (self.users_group_name) + + @classmethod + def get_by_group_name(cls, group_name, cache=False, + case_insensitive=False): + if case_insensitive: + q = cls.query().filter(cls.users_group_name.ilike(group_name)) + else: + q = cls.query().filter(cls.users_group_name == group_name) + if cache: + q = q.options(FromCache( + "sql_cache_short", + "get_user_%s" % _hash_key(group_name) + ) + ) + return q.scalar() + + @classmethod + def get(cls, users_group_id, cache=False): + users_group = cls.query() + if cache: + users_group = users_group.options(FromCache("sql_cache_short", + "get_users_group_%s" % users_group_id)) + return users_group.get(users_group_id) + + def get_api_data(self): + users_group = self + + data = dict( + users_group_id=users_group.users_group_id, + group_name=users_group.users_group_name, + active=users_group.users_group_active, + ) + + return data + + +class UsersGroupMember(Base, BaseModel): + __tablename__ = 'users_groups_members' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + + users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + + user = relationship('User', lazy='joined') + users_group = relationship('UsersGroup') + + def __init__(self, gr_id='', u_id=''): + self.users_group_id = gr_id + self.user_id = u_id + + +class Repository(Base, BaseModel): + __tablename__ = 'repositories' + __table_args__ = ( + UniqueConstraint('repo_name'), + Index('r_repo_name_idx', 'repo_name'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + + repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) + repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) + private = Column("private", Boolean(), nullable=True, unique=None, default=None) + enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True) + enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True) + description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) + updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) + landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None) + enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) + _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) + + fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None) + + user = relationship('User') + fork = relationship('Repository', remote_side=repo_id) + group = relationship('RepoGroup') + repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id') + users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all') + stats = relationship('Statistics', cascade='all', uselist=False) + + followers = relationship('UserFollowing', + primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', + cascade='all') + + logs = relationship('UserLog') + comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan") + + pull_requests_org = relationship('PullRequest', + primaryjoin='PullRequest.org_repo_id==Repository.repo_id', + cascade="all, delete, delete-orphan") + + pull_requests_other = relationship('PullRequest', + primaryjoin='PullRequest.other_repo_id==Repository.repo_id', + cascade="all, delete, delete-orphan") + + def __unicode__(self): + return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, + self.repo_name) + + @hybrid_property + def locked(self): + # always should return [user_id, timelocked] + if self._locked: + _lock_info = self._locked.split(':') + return int(_lock_info[0]), _lock_info[1] + return [None, None] + + @locked.setter + def locked(self, val): + if val and isinstance(val, (list, tuple)): + self._locked = ':'.join(map(str, val)) + else: + self._locked = None + + @classmethod + def url_sep(cls): + return URL_SEP + + @classmethod + def get_by_repo_name(cls, repo_name): + q = Session().query(cls).filter(cls.repo_name == repo_name) + q = q.options(joinedload(Repository.fork))\ + .options(joinedload(Repository.user))\ + .options(joinedload(Repository.group)) + return q.scalar() + + @classmethod + def get_by_full_path(cls, repo_full_path): + repo_name = repo_full_path.split(cls.base_path(), 1)[-1] + return cls.get_by_repo_name(repo_name.strip(URL_SEP)) + + @classmethod + def get_repo_forks(cls, repo_id): + return cls.query().filter(Repository.fork_id == repo_id) + + @classmethod + def base_path(cls): + """ + Returns base path when all repos are stored + + :param cls: + """ + q = Session().query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == cls.url_sep()) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @property + def forks(self): + """ + Return forks of this repo + """ + return Repository.get_repo_forks(self.repo_id) + + @property + def parent(self): + """ + Returns fork parent + """ + return self.fork + + @property + def just_name(self): + return self.repo_name.split(Repository.url_sep())[-1] + + @property + def groups_with_parents(self): + groups = [] + if self.group is None: + return groups + + cur_gr = self.group + groups.insert(0, cur_gr) + while 1: + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + groups.insert(0, gr) + + return groups + + @property + def groups_and_repo(self): + return self.groups_with_parents, self.just_name + + @LazyProperty + def repo_path(self): + """ + Returns base full path for that repository means where it actually + exists on a filesystem + """ + q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == + Repository.url_sep()) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @property + def repo_full_path(self): + p = [self.repo_path] + # we need to split the name by / since this is how we store the + # names in the database, but that eventually needs to be converted + # into a valid system path + p += self.repo_name.split(Repository.url_sep()) + return os.path.join(*p) + + @property + def cache_keys(self): + """ + Returns associated cache keys for that repo + """ + return CacheInvalidation.query()\ + .filter(CacheInvalidation.cache_args == self.repo_name)\ + .order_by(CacheInvalidation.cache_key)\ + .all() + + def get_new_name(self, repo_name): + """ + returns new full repository name based on assigned group and new new + + :param group_name: + """ + path_prefix = self.group.full_path_splitted if self.group else [] + return Repository.url_sep().join(path_prefix + [repo_name]) + + @property + def _ui(self): + """ + Creates an db based ui object for this repository + """ + from rhodecode.lib.utils import make_ui + return make_ui('db', clear_session=False) + + @classmethod + def inject_ui(cls, repo, extras={}): + from rhodecode.lib.vcs.backends.hg import MercurialRepository + from rhodecode.lib.vcs.backends.git import GitRepository + required = (MercurialRepository, GitRepository) + if not isinstance(repo, required): + raise Exception('repo must be instance of %s' % required) + + # inject ui extra param to log this action via push logger + for k, v in extras.items(): + repo._repo.ui.setconfig('rhodecode_extras', k, v) + + @classmethod + def is_valid(cls, repo_name): + """ + returns True if given repo name is a valid filesystem repository + + :param cls: + :param repo_name: + """ + from rhodecode.lib.utils import is_valid_repo + + return is_valid_repo(repo_name, cls.base_path()) + + def get_api_data(self): + """ + Common function for generating repo api data + + """ + repo = self + data = dict( + repo_id=repo.repo_id, + repo_name=repo.repo_name, + repo_type=repo.repo_type, + clone_uri=repo.clone_uri, + private=repo.private, + created_on=repo.created_on, + description=repo.description, + landing_rev=repo.landing_rev, + owner=repo.user.username, + fork_of=repo.fork.repo_name if repo.fork else None + ) + + return data + + @classmethod + def lock(cls, repo, user_id): + repo.locked = [user_id, time.time()] + Session().add(repo) + Session().commit() + + @classmethod + def unlock(cls, repo): + repo.locked = None + Session().add(repo) + Session().commit() + + @property + def last_db_change(self): + return self.updated_on + + #========================================================================== + # SCM PROPERTIES + #========================================================================== + + def get_changeset(self, rev=None): + return get_changeset_safe(self.scm_instance, rev) + + def get_landing_changeset(self): + """ + Returns landing changeset, or if that doesn't exist returns the tip + """ + cs = self.get_changeset(self.landing_rev) or self.get_changeset() + return cs + + def update_last_change(self, last_change=None): + if last_change is None: + last_change = datetime.datetime.now() + if self.updated_on is None or self.updated_on != last_change: + log.debug('updated repo %s with new date %s' % (self, last_change)) + self.updated_on = last_change + Session().add(self) + Session().commit() + + @property + def tip(self): + return self.get_changeset('tip') + + @property + def author(self): + return self.tip.author + + @property + def last_change(self): + return self.scm_instance.last_change + + def get_comments(self, revisions=None): + """ + Returns comments for this repository grouped by revisions + + :param revisions: filter query by revisions only + """ + cmts = ChangesetComment.query()\ + .filter(ChangesetComment.repo == self) + if revisions: + cmts = cmts.filter(ChangesetComment.revision.in_(revisions)) + grouped = defaultdict(list) + for cmt in cmts.all(): + grouped[cmt.revision].append(cmt) + return grouped + + def statuses(self, revisions=None): + """ + Returns statuses for this repository + + :param revisions: list of revisions to get statuses for + :type revisions: list + """ + + statuses = ChangesetStatus.query()\ + .filter(ChangesetStatus.repo == self)\ + .filter(ChangesetStatus.version == 0) + if revisions: + statuses = statuses.filter(ChangesetStatus.revision.in_(revisions)) + grouped = {} -from rhodecode.model.db import * + #maybe we have open new pullrequest without a status ? + stat = ChangesetStatus.STATUS_UNDER_REVIEW + status_lbl = ChangesetStatus.get_status_lbl(stat) + for pr in PullRequest.query().filter(PullRequest.org_repo == self).all(): + for rev in pr.revisions: + pr_id = pr.pull_request_id + pr_repo = pr.other_repo.repo_name + grouped[rev] = [stat, status_lbl, pr_id, pr_repo] + + for stat in statuses.all(): + pr_id = pr_repo = None + if stat.pull_request: + pr_id = stat.pull_request.pull_request_id + pr_repo = stat.pull_request.other_repo.repo_name + grouped[stat.revision] = [str(stat.status), stat.status_lbl, + pr_id, pr_repo] + return grouped + + #========================================================================== + # SCM CACHE INSTANCE + #========================================================================== + + @property + def invalidate(self): + return CacheInvalidation.invalidate(self.repo_name) + + def set_invalidate(self): + """ + set a cache for invalidation for this instance + """ + CacheInvalidation.set_invalidate(repo_name=self.repo_name) + + @LazyProperty + def scm_instance(self): + import rhodecode + full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache')) + if full_cache: + return self.scm_instance_cached() + return self.__get_instance() + + def scm_instance_cached(self, cache_map=None): + @cache_region('long_term') + def _c(repo_name): + return self.__get_instance() + rn = self.repo_name + log.debug('Getting cached instance of repo') + + if cache_map: + # get using prefilled cache_map + invalidate_repo = cache_map[self.repo_name] + if invalidate_repo: + invalidate_repo = (None if invalidate_repo.cache_active + else invalidate_repo) + else: + # get from invalidate + invalidate_repo = self.invalidate + + if invalidate_repo is not None: + region_invalidate(_c, None, rn) + # update our cache + CacheInvalidation.set_valid(invalidate_repo.cache_key) + return _c(rn) + + def __get_instance(self): + repo_full_path = self.repo_full_path + try: + alias = get_scm(repo_full_path)[0] + log.debug('Creating instance of %s repository' % alias) + backend = get_backend(alias) + except VCSError: + log.error(traceback.format_exc()) + log.error('Perhaps this repository is in db and not in ' + 'filesystem run rescan repositories with ' + '"destroy old data " option from admin panel') + return + + if alias == 'hg': + + repo = backend(safe_str(repo_full_path), create=False, + baseui=self._ui) + # skip hidden web repository + if repo._get_hidden(): + return + else: + repo = backend(repo_full_path, create=False) + + return repo + + +class RepoGroup(Base, BaseModel): + __tablename__ = 'groups' + __table_args__ = ( + UniqueConstraint('group_name', 'group_parent_id'), + CheckConstraint('group_id != group_parent_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + __mapper_args__ = {'order_by': 'group_name'} + + group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) + group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) + group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) + + repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') + users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all') + + parent_group = relationship('RepoGroup', remote_side=group_id) + + def __init__(self, group_name='', parent_group=None): + self.group_name = group_name + self.parent_group = parent_group + + def __unicode__(self): + return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id, + self.group_name) + + @classmethod + def groups_choices(cls, check_perms=False): + from webhelpers.html import literal as _literal + from rhodecode.model.scm import ScmModel + groups = cls.query().all() + if check_perms: + #filter group user have access to, it's done + #magically inside ScmModel based on current user + groups = ScmModel().get_repos_groups(groups) + repo_groups = [('', '')] + sep = ' » ' + _name = lambda k: _literal(sep.join(k)) + + repo_groups.extend([(x.group_id, _name(x.full_path_splitted)) + for x in groups]) + + repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0]) + return repo_groups + + @classmethod + def url_sep(cls): + return URL_SEP + + @classmethod + def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + if case_insensitive: + gr = cls.query()\ + .filter(cls.group_name.ilike(group_name)) + else: + gr = cls.query()\ + .filter(cls.group_name == group_name) + if cache: + gr = gr.options(FromCache( + "sql_cache_short", + "get_group_%s" % _hash_key(group_name) + ) + ) + return gr.scalar() + + @property + def parents(self): + parents_recursion_limit = 5 + groups = [] + if self.parent_group is None: + return groups + cur_gr = self.parent_group + groups.insert(0, cur_gr) + cnt = 0 + while 1: + cnt += 1 + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + if cnt == parents_recursion_limit: + # this will prevent accidental infinit loops + log.error('group nested more than %s' % + parents_recursion_limit) + break + + groups.insert(0, gr) + return groups + + @property + def children(self): + return RepoGroup.query().filter(RepoGroup.parent_group == self) + + @property + def name(self): + return self.group_name.split(RepoGroup.url_sep())[-1] + + @property + def full_path(self): + return self.group_name + + @property + def full_path_splitted(self): + return self.group_name.split(RepoGroup.url_sep()) + + @property + def repositories(self): + return Repository.query()\ + .filter(Repository.group == self)\ + .order_by(Repository.repo_name) + + @property + def repositories_recursive_count(self): + cnt = self.repositories.count() + + def children_count(group): + cnt = 0 + for child in group.children: + cnt += child.repositories.count() + cnt += children_count(child) + return cnt + + return cnt + children_count(self) + + def recursive_groups_and_repos(self): + """ + Recursive return all groups, with repositories in those groups + """ + all_ = [] + + def _get_members(root_gr): + for r in root_gr.repositories: + all_.append(r) + childs = root_gr.children.all() + if childs: + for gr in childs: + all_.append(gr) + _get_members(gr) + + _get_members(self) + return [self] + all_ + + def get_new_name(self, group_name): + """ + returns new full group name based on parent and new name + + :param group_name: + """ + path_prefix = (self.parent_group.full_path_splitted if + self.parent_group else []) + return RepoGroup.url_sep().join(path_prefix + [group_name]) + + +class Permission(Base, BaseModel): + __tablename__ = 'permissions' + __table_args__ = ( + Index('p_perm_name_idx', 'permission_name'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + PERMS = [ + ('repository.none', _('Repository no access')), + ('repository.read', _('Repository read access')), + ('repository.write', _('Repository write access')), + ('repository.admin', _('Repository admin access')), + + ('group.none', _('Repositories Group no access')), + ('group.read', _('Repositories Group read access')), + ('group.write', _('Repositories Group write access')), + ('group.admin', _('Repositories Group admin access')), + + ('hg.admin', _('RhodeCode Administrator')), + ('hg.create.none', _('Repository creation disabled')), + ('hg.create.repository', _('Repository creation enabled')), + ('hg.fork.none', _('Repository forking disabled')), + ('hg.fork.repository', _('Repository forking enabled')), + ('hg.register.none', _('Register disabled')), + ('hg.register.manual_activate', _('Register new user with RhodeCode ' + 'with manual activation')), + + ('hg.register.auto_activate', _('Register new user with RhodeCode ' + 'with auto activation')), + ] + + # defines which permissions are more important higher the more important + PERM_WEIGHTS = { + 'repository.none': 0, + 'repository.read': 1, + 'repository.write': 3, + 'repository.admin': 4, + + 'group.none': 0, + 'group.read': 1, + 'group.write': 3, + 'group.admin': 4, + + 'hg.fork.none': 0, + 'hg.fork.repository': 1, + 'hg.create.none': 0, + 'hg.create.repository':1 + } + + permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + + def __unicode__(self): + return u"<%s('%s:%s')>" % ( + self.__class__.__name__, self.permission_id, self.permission_name + ) + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.permission_name == key).scalar() + + @classmethod + def get_default_perms(cls, default_user_id): + q = Session().query(UserRepoToPerm, Repository, cls)\ + .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ + .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\ + .filter(UserRepoToPerm.user_id == default_user_id) + + return q.all() + + @classmethod + def get_default_group_perms(cls, default_user_id): + q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\ + .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\ + .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\ + .filter(UserRepoGroupToPerm.user_id == default_user_id) + + return q.all() + + +class UserRepoToPerm(Base, BaseModel): + __tablename__ = 'repo_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'repository_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + repository = relationship('Repository') + permission = relationship('Permission') + + @classmethod + def create(cls, user, repository, permission): + n = cls() + n.user = user + n.repository = repository + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.user, self.repository) + + +class UserToPerm(Base, BaseModel): + __tablename__ = 'user_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + permission = relationship('Permission', lazy='joined') + + +class UsersGroupRepoToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_to_perm' + __table_args__ = ( + UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UsersGroup') + permission = relationship('Permission') + repository = relationship('Repository') + + @classmethod + def create(cls, users_group, repository, permission): + n = cls() + n.users_group = users_group + n.repository = repository + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.users_group, self.repository) + + +class UsersGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'permission_id',), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UsersGroup') + permission = relationship('Permission') + + +class UserRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'user_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + + group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + group = relationship('RepoGroup') + permission = relationship('Permission') + + +class UsersGroupRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'group_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + + users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UsersGroup') + permission = relationship('Permission') + group = relationship('RepoGroup') + + +class Statistics(Base, BaseModel): + __tablename__ = 'statistics' + __table_args__ = ( + UniqueConstraint('repository_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None) + stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) + commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data + commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data + languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data + + repository = relationship('Repository', single_parent=True) + + +class UserFollowing(Base, BaseModel): + __tablename__ = 'user_followings' + __table_args__ = ( + UniqueConstraint('user_id', 'follows_repository_id'), + UniqueConstraint('user_id', 'follows_user_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + + user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None) + follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) + + user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id') + + follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') + follows_repository = relationship('Repository', order_by='Repository.repo_name') + + @classmethod + def get_repo_followers(cls, repo_id): + return cls.query().filter(cls.follows_repo_id == repo_id) + + +class CacheInvalidation(Base, BaseModel): + __tablename__ = 'cache_invalidation' + __table_args__ = ( + UniqueConstraint('cache_key'), + Index('key_idx', 'cache_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) + cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) + + def __init__(self, cache_key, cache_args=''): + self.cache_key = cache_key + self.cache_args = cache_args + self.cache_active = False + + def __unicode__(self): + return u"<%s('%s:%s')>" % (self.__class__.__name__, + self.cache_id, self.cache_key) + + @property + def prefix(self): + _split = self.cache_key.split(self.cache_args, 1) + if _split and len(_split) == 2: + return _split[0] + return '' + + @classmethod + def clear_cache(cls): + cls.query().delete() + + @classmethod + def _get_key(cls, key): + """ + Wrapper for generating a key, together with a prefix + + :param key: + """ + import rhodecode + prefix = '' + org_key = key + iid = rhodecode.CONFIG.get('instance_id') + if iid: + prefix = iid + + return "%s%s" % (prefix, key), prefix, org_key + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.cache_key == key).scalar() + + @classmethod + def get_by_repo_name(cls, repo_name): + return cls.query().filter(cls.cache_args == repo_name).all() + + @classmethod + def _get_or_create_key(cls, key, repo_name, commit=True): + inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar() + if not inv_obj: + try: + inv_obj = CacheInvalidation(key, repo_name) + Session().add(inv_obj) + if commit: + Session().commit() + except Exception: + log.error(traceback.format_exc()) + Session().rollback() + return inv_obj + + @classmethod + def invalidate(cls, key): + """ + Returns Invalidation object if this given key should be invalidated + None otherwise. `cache_active = False` means that this cache + state is not valid and needs to be invalidated + + :param key: + """ + repo_name = key + repo_name = remove_suffix(repo_name, '_README') + repo_name = remove_suffix(repo_name, '_RSS') + repo_name = remove_suffix(repo_name, '_ATOM') + + # adds instance prefix + key, _prefix, _org_key = cls._get_key(key) + inv = cls._get_or_create_key(key, repo_name) + + if inv and inv.cache_active is False: + return inv + + @classmethod + def set_invalidate(cls, key=None, repo_name=None): + """ + Mark this Cache key for invalidation, either by key or whole + cache sets based on repo_name + + :param key: + """ + if key: + key, _prefix, _org_key = cls._get_key(key) + inv_objs = Session().query(cls).filter(cls.cache_key == key).all() + elif repo_name: + inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all() + + log.debug('marking %s key[s] for invalidation based on key=%s,repo_name=%s' + % (len(inv_objs), key, repo_name)) + try: + for inv_obj in inv_objs: + inv_obj.cache_active = False + Session().add(inv_obj) + Session().commit() + except Exception: + log.error(traceback.format_exc()) + Session().rollback() + + @classmethod + def set_valid(cls, key): + """ + Mark this cache key as active and currently cached + + :param key: + """ + inv_obj = cls.get_by_key(key) + inv_obj.cache_active = True + Session().add(inv_obj) + Session().commit() + + @classmethod + def get_cache_map(cls): + + class cachemapdict(dict): + + def __init__(self, *args, **kwargs): + fixkey = kwargs.get('fixkey') + if fixkey: + del kwargs['fixkey'] + self.fixkey = fixkey + super(cachemapdict, self).__init__(*args, **kwargs) + + def __getattr__(self, name): + key = name + if self.fixkey: + key, _prefix, _org_key = cls._get_key(key) + if key in self.__dict__: + return self.__dict__[key] + else: + return self[key] + + def __getitem__(self, key): + if self.fixkey: + key, _prefix, _org_key = cls._get_key(key) + try: + return super(cachemapdict, self).__getitem__(key) + except KeyError: + return + + cache_map = cachemapdict(fixkey=True) + for obj in cls.query().all(): + cache_map[obj.cache_key] = cachemapdict(obj.get_dict()) + return cache_map + + +class ChangesetComment(Base, BaseModel): + __tablename__ = 'changeset_comments' + __table_args__ = ( + Index('cc_revision_idx', 'revision'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True) + repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + revision = Column('revision', String(40), nullable=True) + pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) + line_no = Column('line_no', Unicode(10), nullable=True) + hl_lines = Column('hl_lines', Unicode(512), nullable=True) + f_path = Column('f_path', Unicode(1000), nullable=True) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False) + text = Column('text', UnicodeText(25000), nullable=False) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + + author = relationship('User', lazy='joined') + repo = relationship('Repository') + status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan") + pull_request = relationship('PullRequest', lazy='joined') + + @classmethod + def get_users(cls, revision=None, pull_request_id=None): + """ + Returns user associated with this ChangesetComment. ie those + who actually commented + + :param cls: + :param revision: + """ + q = Session().query(User)\ + .join(ChangesetComment.author) + if revision: + q = q.filter(cls.revision == revision) + elif pull_request_id: + q = q.filter(cls.pull_request_id == pull_request_id) + return q.all() + + +class ChangesetStatus(Base, BaseModel): + __tablename__ = 'changeset_statuses' + __table_args__ = ( + Index('cs_revision_idx', 'revision'), + Index('cs_version_idx', 'version'), + UniqueConstraint('repo_id', 'revision', 'version'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed' + STATUS_APPROVED = 'approved' + STATUS_REJECTED = 'rejected' + STATUS_UNDER_REVIEW = 'under_review' + + STATUSES = [ + (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default + (STATUS_APPROVED, _("Approved")), + (STATUS_REJECTED, _("Rejected")), + (STATUS_UNDER_REVIEW, _("Under Review")), + ] + + changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True) + repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None) + revision = Column('revision', String(40), nullable=False) + status = Column('status', String(128), nullable=False, default=DEFAULT) + changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id')) + modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now) + version = Column('version', Integer(), nullable=False, default=0) + pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) + + author = relationship('User', lazy='joined') + repo = relationship('Repository') + comment = relationship('ChangesetComment', lazy='joined') + pull_request = relationship('PullRequest', lazy='joined') + + def __unicode__(self): + return u"<%s('%s:%s')>" % ( + self.__class__.__name__, + self.status, self.author + ) + + @classmethod + def get_status_lbl(cls, value): + return dict(cls.STATUSES).get(value) + + @property + def status_lbl(self): + return ChangesetStatus.get_status_lbl(self.status) + + +class PullRequest(Base, BaseModel): + __tablename__ = 'pull_requests' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + + STATUS_NEW = u'new' + STATUS_OPEN = u'open' + STATUS_CLOSED = u'closed' + + pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True) + title = Column('title', Unicode(256), nullable=True) + description = Column('description', UnicodeText(10240), nullable=True) + status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None) + _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max + org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + org_ref = Column('org_ref', Unicode(256), nullable=False) + other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + other_ref = Column('other_ref', Unicode(256), nullable=False) + + @hybrid_property + def revisions(self): + return self._revisions.split(':') + + @revisions.setter + def revisions(self, val): + self._revisions = ':'.join(val) + + author = relationship('User', lazy='joined') + reviewers = relationship('PullRequestReviewers', + cascade="all, delete, delete-orphan") + org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id') + other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id') + statuses = relationship('ChangesetStatus') + comments = relationship('ChangesetComment', + cascade="all, delete, delete-orphan") + + def is_closed(self): + return self.status == self.STATUS_CLOSED + + def __json__(self): + return dict( + revisions=self.revisions + ) + + +class PullRequestReviewers(Base, BaseModel): + __tablename__ = 'pull_request_reviewers' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + + def __init__(self, user=None, pull_request=None): + self.user = user + self.pull_request = pull_request + + pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True) + pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True) + + user = relationship('User') + pull_request = relationship('PullRequest') + + +class Notification(Base, BaseModel): + __tablename__ = 'notifications' + __table_args__ = ( + Index('notification_type_idx', 'type'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + + TYPE_CHANGESET_COMMENT = u'cs_comment' + TYPE_MESSAGE = u'message' + TYPE_MENTION = u'mention' + TYPE_REGISTRATION = u'registration' + TYPE_PULL_REQUEST = u'pull_request' + TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment' + + notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) + subject = Column('subject', Unicode(512), nullable=True) + body = Column('body', UnicodeText(50000), nullable=True) + created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + type_ = Column('type', Unicode(256)) + + created_by_user = relationship('User') + notifications_to_users = relationship('UserNotification', lazy='joined', + cascade="all, delete, delete-orphan") + + @property + def recipients(self): + return [x.user for x in UserNotification.query()\ + .filter(UserNotification.notification == self)\ + .order_by(UserNotification.user_id.asc()).all()] + + @classmethod + def create(cls, created_by, subject, body, recipients, type_=None): + if type_ is None: + type_ = Notification.TYPE_MESSAGE + + notification = cls() + notification.created_by_user = created_by + notification.subject = subject + notification.body = body + notification.type_ = type_ + notification.created_on = datetime.datetime.now() + + for u in recipients: + assoc = UserNotification() + assoc.notification = notification + u.notifications.append(assoc) + Session().add(notification) + return notification + + @property + def description(self): + from rhodecode.model.notification import NotificationModel + return NotificationModel().make_description(self) + + +class UserNotification(Base, BaseModel): + __tablename__ = 'user_to_notification' + __table_args__ = ( + UniqueConstraint('user_id', 'notification_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) + notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True) + read = Column('read', Boolean, default=False) + sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None) + + user = relationship('User', lazy="joined") + notification = relationship('Notification', lazy="joined", + order_by=lambda: Notification.created_on.desc(),) + + def mark_as_read(self): + self.read = True + Session().add(self) + + +class DbMigrateVersion(Base, BaseModel): + __tablename__ = 'db_migrate_version' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}, + ) + repository_id = Column('repository_id', String(250), primary_key=True) + repository_path = Column('repository_path', Text) + version = Column('version', Integer) diff --git a/rhodecode/lib/dbmigrate/schema/db_1_5_2.py b/rhodecode/lib/dbmigrate/schema/db_1_5_2.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/schema/db_1_5_2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.model.db_1_4_0 + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Database Models for RhodeCode <=1.5.X + + :created_on: Apr 08, 2010 + :author: marcink + :copyright: (C) 2010-2012 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +#TODO: replace that will db.py content after 1.6 Release + +from rhodecode.model.db import * diff --git a/rhodecode/lib/dbmigrate/versions/006_version_1_4_0.py b/rhodecode/lib/dbmigrate/versions/006_version_1_4_0.py --- a/rhodecode/lib/dbmigrate/versions/006_version_1_4_0.py +++ b/rhodecode/lib/dbmigrate/versions/006_version_1_4_0.py @@ -12,6 +12,7 @@ from rhodecode.lib.dbmigrate.migrate.cha from rhodecode.model.meta import Base from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base log = logging.getLogger(__name__) @@ -49,12 +50,7 @@ def upgrade(migrate_engine): tbl = ChangesetStatus.__table__ tbl.create() - ## RESET COMPLETLY THE metadata for sqlalchemy to use the 1_3_0 Base - Base = declarative_base() - Base.metadata.clear() - Base.metadata = MetaData() - Base.metadata.bind = migrate_engine - meta.Base = Base + _reset_base(migrate_engine) #========================================================================== # USERS TABLE @@ -173,12 +169,7 @@ def upgrade(migrate_engine): ForeignKey('pull_requests.pull_request_id'), nullable=True) pull_request_id.create(table=tbl) - ## RESET COMPLETLY THE metadata for sqlalchemy back after using 1_3_0 - Base = declarative_base() - Base.metadata.clear() - Base.metadata = MetaData() - Base.metadata.bind = migrate_engine - meta.Base = Base + _reset_base(migrate_engine) def downgrade(migrate_engine): diff --git a/rhodecode/lib/dbmigrate/versions/008_version_1_5_0.py b/rhodecode/lib/dbmigrate/versions/008_version_1_5_0.py --- a/rhodecode/lib/dbmigrate/versions/008_version_1_5_0.py +++ b/rhodecode/lib/dbmigrate/versions/008_version_1_5_0.py @@ -12,6 +12,7 @@ from rhodecode.lib.dbmigrate.migrate.cha from rhodecode.model.meta import Base from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base log = logging.getLogger(__name__) @@ -24,6 +25,7 @@ def upgrade(migrate_engine): #========================================================================== # USER LOGS #========================================================================== + _reset_base(migrate_engine) from rhodecode.lib.dbmigrate.schema.db_1_5_0 import UserLog tbl = UserLog.__table__ username = Column("username", String(255, convert_unicode=False, diff --git a/rhodecode/lib/dbmigrate/versions/010_version_1_5_2.py b/rhodecode/lib/dbmigrate/versions/010_version_1_5_2.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/010_version_1_5_2.py @@ -0,0 +1,50 @@ +import logging +import datetime + +from sqlalchemy import * +from sqlalchemy.exc import DatabaseError +from sqlalchemy.orm import relation, backref, class_mapper, joinedload +from sqlalchemy.orm.session import Session +from sqlalchemy.ext.declarative import declarative_base + +from rhodecode.lib.dbmigrate.migrate import * +from rhodecode.lib.dbmigrate.migrate.changeset import * + +from rhodecode.model.meta import Base +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base + +log = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + """ + Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + _reset_base(migrate_engine) + #========================================================================== + # USER LOGS + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_5_2 import UserIpMap + tbl = UserIpMap.__table__ + tbl.create() + + #========================================================================== + # REPOSITORIES + #========================================================================== + from rhodecode.lib.dbmigrate.schema.db_1_5_2 import Repository + tbl = Repository.__table__ + changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) + # create username column + changeset_cache.create(table=tbl) + + #fix cache data + repositories = Repository.getAll() + for entry in repositories: + entry.update_changeset_cache() + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine diff --git a/rhodecode/lib/dbmigrate/versions/__init__.py b/rhodecode/lib/dbmigrate/versions/__init__.py --- a/rhodecode/lib/dbmigrate/versions/__init__.py +++ b/rhodecode/lib/dbmigrate/versions/__init__.py @@ -22,3 +22,23 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from sqlalchemy import * +from sqlalchemy.exc import DatabaseError +from sqlalchemy.orm import relation, backref, class_mapper, joinedload +from sqlalchemy.orm.session import Session +from sqlalchemy.ext.declarative import declarative_base + +from rhodecode.lib.dbmigrate.migrate import * +from rhodecode.lib.dbmigrate.migrate.changeset import * + +from rhodecode.model.meta import Base +from rhodecode.model import meta + + +def _reset_base(migrate_engine): + ## RESET COMPLETLY THE metadata for sqlalchemy to use previous declared Base + Base = declarative_base() + Base.metadata.clear() + Base.metadata = MetaData() + Base.metadata.bind = migrate_engine + meta.Base = Base diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -583,7 +583,7 @@ class DiffProcessor(object): #return u''.join(imap(self._line_counter, self._diff.splitlines(1))) def as_html(self, table_class='code-difftable', line_class='line', - new_lineno_class='lineno old', old_lineno_class='lineno new', + old_lineno_class='lineno old', new_lineno_class='lineno new', code_class='code', enable_comments=False, parsed_lines=None): """ Return given diff as html table with customized css classes diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -464,7 +464,7 @@ def desc_stylize(value): '
see => \\1
', value) value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]', '', value) - value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z\-\/]*)\]', + value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]', '
\\1 => \\2
', value) value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]', '
\\2
', value) @@ -1164,3 +1164,9 @@ def not_mapped_error(repo_name): ' it was created or renamed from the filesystem' ' please run the application again' ' in order to rescan repositories') % repo_name, category='error') + + +def ip_range(ip_addr): + from rhodecode.model.db import UserIpMap + s, e = UserIpMap._get_ip_range(ip_addr) + return '%s - %s' % (s, e) diff --git a/rhodecode/lib/ipaddr.py b/rhodecode/lib/ipaddr.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/ipaddr.py @@ -0,0 +1,1901 @@ +# Copyright 2007 Google Inc. +# Licensed to PSF under a Contributor Agreement. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""A fast, lightweight IPv4/IPv6 manipulation library in Python. + +This library is used to create/poke/manipulate IPv4 and IPv6 addresses +and networks. + +""" + +__version__ = 'trunk' + +import struct + +IPV4LENGTH = 32 +IPV6LENGTH = 128 + + +class AddressValueError(ValueError): + """A Value Error related to the address.""" + + +class NetmaskValueError(ValueError): + """A Value Error related to the netmask.""" + + +def IPAddress(address, version=None): + """Take an IP string/int and return an object of the correct type. + + Args: + address: A string or integer, the IP address. Either IPv4 or + IPv6 addresses may be supplied; integers less than 2**32 will + be considered to be IPv4 by default. + version: An Integer, 4 or 6. If set, don't try to automatically + determine what the IP address type is. important for things + like IPAddress(1), which could be IPv4, '0.0.0.1', or IPv6, + '::1'. + + Returns: + An IPv4Address or IPv6Address object. + + Raises: + ValueError: if the string passed isn't either a v4 or a v6 + address. + + """ + if version: + if version == 4: + return IPv4Address(address) + elif version == 6: + return IPv6Address(address) + + try: + return IPv4Address(address) + except (AddressValueError, NetmaskValueError): + pass + + try: + return IPv6Address(address) + except (AddressValueError, NetmaskValueError): + pass + + raise ValueError('%r does not appear to be an IPv4 or IPv6 address' % + address) + + +def IPNetwork(address, version=None, strict=False): + """Take an IP string/int and return an object of the correct type. + + Args: + address: A string or integer, the IP address. Either IPv4 or + IPv6 addresses may be supplied; integers less than 2**32 will + be considered to be IPv4 by default. + version: An Integer, if set, don't try to automatically + determine what the IP address type is. important for things + like IPNetwork(1), which could be IPv4, '0.0.0.1/32', or IPv6, + '::1/128'. + + Returns: + An IPv4Network or IPv6Network object. + + Raises: + ValueError: if the string passed isn't either a v4 or a v6 + address. Or if a strict network was requested and a strict + network wasn't given. + + """ + if version: + if version == 4: + return IPv4Network(address, strict) + elif version == 6: + return IPv6Network(address, strict) + + try: + return IPv4Network(address, strict) + except (AddressValueError, NetmaskValueError): + pass + + try: + return IPv6Network(address, strict) + except (AddressValueError, NetmaskValueError): + pass + + raise ValueError('%r does not appear to be an IPv4 or IPv6 network' % + address) + + +def v4_int_to_packed(address): + """The binary representation of this address. + + Args: + address: An integer representation of an IPv4 IP address. + + Returns: + The binary representation of this address. + + Raises: + ValueError: If the integer is too large to be an IPv4 IP + address. + """ + if address > _BaseV4._ALL_ONES: + raise ValueError('Address too large for IPv4') + return Bytes(struct.pack('!I', address)) + + +def v6_int_to_packed(address): + """The binary representation of this address. + + Args: + address: An integer representation of an IPv6 IP address. + + Returns: + The binary representation of this address. + """ + return Bytes(struct.pack('!QQ', address >> 64, address & (2 ** 64 - 1))) + + +def _find_address_range(addresses): + """Find a sequence of addresses. + + Args: + addresses: a list of IPv4 or IPv6 addresses. + + Returns: + A tuple containing the first and last IP addresses in the sequence. + + """ + first = last = addresses[0] + for ip in addresses[1:]: + if ip._ip == last._ip + 1: + last = ip + else: + break + return (first, last) + + +def _get_prefix_length(number1, number2, bits): + """Get the number of leading bits that are same for two numbers. + + Args: + number1: an integer. + number2: another integer. + bits: the maximum number of bits to compare. + + Returns: + The number of leading bits that are the same for two numbers. + + """ + for i in range(bits): + if number1 >> i == number2 >> i: + return bits - i + return 0 + + +def _count_righthand_zero_bits(number, bits): + """Count the number of zero bits on the right hand side. + + Args: + number: an integer. + bits: maximum number of bits to count. + + Returns: + The number of zero bits on the right hand side of the number. + + """ + if number == 0: + return bits + for i in range(bits): + if (number >> i) % 2: + return i + + +def summarize_address_range(first, last): + """Summarize a network range given the first and last IP addresses. + + Example: + >>> summarize_address_range(IPv4Address('1.1.1.0'), + IPv4Address('1.1.1.130')) + [IPv4Network('1.1.1.0/25'), IPv4Network('1.1.1.128/31'), + IPv4Network('1.1.1.130/32')] + + Args: + first: the first IPv4Address or IPv6Address in the range. + last: the last IPv4Address or IPv6Address in the range. + + Returns: + The address range collapsed to a list of IPv4Network's or + IPv6Network's. + + Raise: + TypeError: + If the first and last objects are not IP addresses. + If the first and last objects are not the same version. + ValueError: + If the last object is not greater than the first. + If the version is not 4 or 6. + + """ + if not (isinstance(first, _BaseIP) and isinstance(last, _BaseIP)): + raise TypeError('first and last must be IP addresses, not networks') + if first.version != last.version: + raise TypeError("%s and %s are not of the same version" % ( + str(first), str(last))) + if first > last: + raise ValueError('last IP address must be greater than first') + + networks = [] + + if first.version == 4: + ip = IPv4Network + elif first.version == 6: + ip = IPv6Network + else: + raise ValueError('unknown IP version') + + ip_bits = first._max_prefixlen + first_int = first._ip + last_int = last._ip + while first_int <= last_int: + nbits = _count_righthand_zero_bits(first_int, ip_bits) + current = None + while nbits >= 0: + addend = 2 ** nbits - 1 + current = first_int + addend + nbits -= 1 + if current <= last_int: + break + prefix = _get_prefix_length(first_int, current, ip_bits) + net = ip('%s/%d' % (str(first), prefix)) + networks.append(net) + if current == ip._ALL_ONES: + break + first_int = current + 1 + first = IPAddress(first_int, version=first._version) + return networks + + +def _collapse_address_list_recursive(addresses): + """Loops through the addresses, collapsing concurrent netblocks. + + Example: + + ip1 = IPv4Network('1.1.0.0/24') + ip2 = IPv4Network('1.1.1.0/24') + ip3 = IPv4Network('1.1.2.0/24') + ip4 = IPv4Network('1.1.3.0/24') + ip5 = IPv4Network('1.1.4.0/24') + ip6 = IPv4Network('1.1.0.1/22') + + _collapse_address_list_recursive([ip1, ip2, ip3, ip4, ip5, ip6]) -> + [IPv4Network('1.1.0.0/22'), IPv4Network('1.1.4.0/24')] + + This shouldn't be called directly; it is called via + collapse_address_list([]). + + Args: + addresses: A list of IPv4Network's or IPv6Network's + + Returns: + A list of IPv4Network's or IPv6Network's depending on what we were + passed. + + """ + ret_array = [] + optimized = False + + for cur_addr in addresses: + if not ret_array: + ret_array.append(cur_addr) + continue + if cur_addr in ret_array[-1]: + optimized = True + elif cur_addr == ret_array[-1].supernet().subnet()[1]: + ret_array.append(ret_array.pop().supernet()) + optimized = True + else: + ret_array.append(cur_addr) + + if optimized: + return _collapse_address_list_recursive(ret_array) + + return ret_array + + +def collapse_address_list(addresses): + """Collapse a list of IP objects. + + Example: + collapse_address_list([IPv4('1.1.0.0/24'), IPv4('1.1.1.0/24')]) -> + [IPv4('1.1.0.0/23')] + + Args: + addresses: A list of IPv4Network or IPv6Network objects. + + Returns: + A list of IPv4Network or IPv6Network objects depending on what we + were passed. + + Raises: + TypeError: If passed a list of mixed version objects. + + """ + i = 0 + addrs = [] + ips = [] + nets = [] + + # split IP addresses and networks + for ip in addresses: + if isinstance(ip, _BaseIP): + if ips and ips[-1]._version != ip._version: + raise TypeError("%s and %s are not of the same version" % ( + str(ip), str(ips[-1]))) + ips.append(ip) + elif ip._prefixlen == ip._max_prefixlen: + if ips and ips[-1]._version != ip._version: + raise TypeError("%s and %s are not of the same version" % ( + str(ip), str(ips[-1]))) + ips.append(ip.ip) + else: + if nets and nets[-1]._version != ip._version: + raise TypeError("%s and %s are not of the same version" % ( + str(ip), str(nets[-1]))) + nets.append(ip) + + # sort and dedup + ips = sorted(set(ips)) + nets = sorted(set(nets)) + + while i < len(ips): + (first, last) = _find_address_range(ips[i:]) + i = ips.index(last) + 1 + addrs.extend(summarize_address_range(first, last)) + + return _collapse_address_list_recursive(sorted( + addrs + nets, key=_BaseNet._get_networks_key)) + +# backwards compatibility +CollapseAddrList = collapse_address_list + +# We need to distinguish between the string and packed-bytes representations +# of an IP address. For example, b'0::1' is the IPv4 address 48.58.58.49, +# while '0::1' is an IPv6 address. +# +# In Python 3, the native 'bytes' type already provides this functionality, +# so we use it directly. For earlier implementations where bytes is not a +# distinct type, we create a subclass of str to serve as a tag. +# +# Usage example (Python 2): +# ip = ipaddr.IPAddress(ipaddr.Bytes('xxxx')) +# +# Usage example (Python 3): +# ip = ipaddr.IPAddress(b'xxxx') +try: + if bytes is str: + raise TypeError("bytes is not a distinct type") + Bytes = bytes +except (NameError, TypeError): + class Bytes(str): + def __repr__(self): + return 'Bytes(%s)' % str.__repr__(self) + + +def get_mixed_type_key(obj): + """Return a key suitable for sorting between networks and addresses. + + Address and Network objects are not sortable by default; they're + fundamentally different so the expression + + IPv4Address('1.1.1.1') <= IPv4Network('1.1.1.1/24') + + doesn't make any sense. There are some times however, where you may wish + to have ipaddr sort these for you anyway. If you need to do this, you + can use this function as the key= argument to sorted(). + + Args: + obj: either a Network or Address object. + Returns: + appropriate key. + + """ + if isinstance(obj, _BaseNet): + return obj._get_networks_key() + elif isinstance(obj, _BaseIP): + return obj._get_address_key() + return NotImplemented + + +class _IPAddrBase(object): + + """The mother class.""" + + def __index__(self): + return self._ip + + def __int__(self): + return self._ip + + def __hex__(self): + return hex(self._ip) + + @property + def exploded(self): + """Return the longhand version of the IP address as a string.""" + return self._explode_shorthand_ip_string() + + @property + def compressed(self): + """Return the shorthand version of the IP address as a string.""" + return str(self) + + +class _BaseIP(_IPAddrBase): + + """A generic IP object. + + This IP class contains the version independent methods which are + used by single IP addresses. + + """ + + def __eq__(self, other): + try: + return (self._ip == other._ip + and self._version == other._version) + except AttributeError: + return NotImplemented + + def __ne__(self, other): + eq = self.__eq__(other) + if eq is NotImplemented: + return NotImplemented + return not eq + + def __le__(self, other): + gt = self.__gt__(other) + if gt is NotImplemented: + return NotImplemented + return not gt + + def __ge__(self, other): + lt = self.__lt__(other) + if lt is NotImplemented: + return NotImplemented + return not lt + + def __lt__(self, other): + if self._version != other._version: + raise TypeError('%s and %s are not of the same version' % ( + str(self), str(other))) + if not isinstance(other, _BaseIP): + raise TypeError('%s and %s are not of the same type' % ( + str(self), str(other))) + if self._ip != other._ip: + return self._ip < other._ip + return False + + def __gt__(self, other): + if self._version != other._version: + raise TypeError('%s and %s are not of the same version' % ( + str(self), str(other))) + if not isinstance(other, _BaseIP): + raise TypeError('%s and %s are not of the same type' % ( + str(self), str(other))) + if self._ip != other._ip: + return self._ip > other._ip + return False + + # Shorthand for Integer addition and subtraction. This is not + # meant to ever support addition/subtraction of addresses. + def __add__(self, other): + if not isinstance(other, int): + return NotImplemented + return IPAddress(int(self) + other, version=self._version) + + def __sub__(self, other): + if not isinstance(other, int): + return NotImplemented + return IPAddress(int(self) - other, version=self._version) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, str(self)) + + def __str__(self): + return '%s' % self._string_from_ip_int(self._ip) + + def __hash__(self): + return hash(hex(long(self._ip))) + + def _get_address_key(self): + return (self._version, self) + + @property + def version(self): + raise NotImplementedError('BaseIP has no version') + + +class _BaseNet(_IPAddrBase): + + """A generic IP object. + + This IP class contains the version independent methods which are + used by networks. + + """ + + def __init__(self, address): + self._cache = {} + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, str(self)) + + def iterhosts(self): + """Generate Iterator over usable hosts in a network. + + This is like __iter__ except it doesn't return the network + or broadcast addresses. + + """ + cur = int(self.network) + 1 + bcast = int(self.broadcast) - 1 + while cur <= bcast: + cur += 1 + yield IPAddress(cur - 1, version=self._version) + + def __iter__(self): + cur = int(self.network) + bcast = int(self.broadcast) + while cur <= bcast: + cur += 1 + yield IPAddress(cur - 1, version=self._version) + + def __getitem__(self, n): + network = int(self.network) + broadcast = int(self.broadcast) + if n >= 0: + if network + n > broadcast: + raise IndexError + return IPAddress(network + n, version=self._version) + else: + n += 1 + if broadcast + n < network: + raise IndexError + return IPAddress(broadcast + n, version=self._version) + + def __lt__(self, other): + if self._version != other._version: + raise TypeError('%s and %s are not of the same version' % ( + str(self), str(other))) + if not isinstance(other, _BaseNet): + raise TypeError('%s and %s are not of the same type' % ( + str(self), str(other))) + if self.network != other.network: + return self.network < other.network + if self.netmask != other.netmask: + return self.netmask < other.netmask + return False + + def __gt__(self, other): + if self._version != other._version: + raise TypeError('%s and %s are not of the same version' % ( + str(self), str(other))) + if not isinstance(other, _BaseNet): + raise TypeError('%s and %s are not of the same type' % ( + str(self), str(other))) + if self.network != other.network: + return self.network > other.network + if self.netmask != other.netmask: + return self.netmask > other.netmask + return False + + def __le__(self, other): + gt = self.__gt__(other) + if gt is NotImplemented: + return NotImplemented + return not gt + + def __ge__(self, other): + lt = self.__lt__(other) + if lt is NotImplemented: + return NotImplemented + return not lt + + def __eq__(self, other): + try: + return (self._version == other._version + and self.network == other.network + and int(self.netmask) == int(other.netmask)) + except AttributeError: + if isinstance(other, _BaseIP): + return (self._version == other._version + and self._ip == other._ip) + + def __ne__(self, other): + eq = self.__eq__(other) + if eq is NotImplemented: + return NotImplemented + return not eq + + def __str__(self): + return '%s/%s' % (str(self.ip), + str(self._prefixlen)) + + def __hash__(self): + return hash(int(self.network) ^ int(self.netmask)) + + def __contains__(self, other): + # always false if one is v4 and the other is v6. + if self._version != other._version: + return False + # dealing with another network. + if isinstance(other, _BaseNet): + return (self.network <= other.network and + self.broadcast >= other.broadcast) + # dealing with another address + else: + return (int(self.network) <= int(other._ip) <= + int(self.broadcast)) + + def overlaps(self, other): + """Tell if self is partly contained in other.""" + return self.network in other or self.broadcast in other or ( + other.network in self or other.broadcast in self) + + @property + def network(self): + x = self._cache.get('network') + if x is None: + x = IPAddress(self._ip & int(self.netmask), version=self._version) + self._cache['network'] = x + return x + + @property + def broadcast(self): + x = self._cache.get('broadcast') + if x is None: + x = IPAddress(self._ip | int(self.hostmask), version=self._version) + self._cache['broadcast'] = x + return x + + @property + def hostmask(self): + x = self._cache.get('hostmask') + if x is None: + x = IPAddress(int(self.netmask) ^ self._ALL_ONES, + version=self._version) + self._cache['hostmask'] = x + return x + + @property + def with_prefixlen(self): + return '%s/%d' % (str(self.ip), self._prefixlen) + + @property + def with_netmask(self): + return '%s/%s' % (str(self.ip), str(self.netmask)) + + @property + def with_hostmask(self): + return '%s/%s' % (str(self.ip), str(self.hostmask)) + + @property + def numhosts(self): + """Number of hosts in the current subnet.""" + return int(self.broadcast) - int(self.network) + 1 + + @property + def version(self): + raise NotImplementedError('BaseNet has no version') + + @property + def prefixlen(self): + return self._prefixlen + + def address_exclude(self, other): + """Remove an address from a larger block. + + For example: + + addr1 = IPNetwork('10.1.1.0/24') + addr2 = IPNetwork('10.1.1.0/26') + addr1.address_exclude(addr2) = + [IPNetwork('10.1.1.64/26'), IPNetwork('10.1.1.128/25')] + + or IPv6: + + addr1 = IPNetwork('::1/32') + addr2 = IPNetwork('::1/128') + addr1.address_exclude(addr2) = [IPNetwork('::0/128'), + IPNetwork('::2/127'), + IPNetwork('::4/126'), + IPNetwork('::8/125'), + ... + IPNetwork('0:0:8000::/33')] + + Args: + other: An IPvXNetwork object of the same type. + + Returns: + A sorted list of IPvXNetwork objects addresses which is self + minus other. + + Raises: + TypeError: If self and other are of difffering address + versions, or if other is not a network object. + ValueError: If other is not completely contained by self. + + """ + if not self._version == other._version: + raise TypeError("%s and %s are not of the same version" % ( + str(self), str(other))) + + if not isinstance(other, _BaseNet): + raise TypeError("%s is not a network object" % str(other)) + + if other not in self: + raise ValueError('%s not contained in %s' % (str(other), + str(self))) + if other == self: + return [] + + ret_addrs = [] + + # Make sure we're comparing the network of other. + other = IPNetwork('%s/%s' % (str(other.network), str(other.prefixlen)), + version=other._version) + + s1, s2 = self.subnet() + while s1 != other and s2 != other: + if other in s1: + ret_addrs.append(s2) + s1, s2 = s1.subnet() + elif other in s2: + ret_addrs.append(s1) + s1, s2 = s2.subnet() + else: + # If we got here, there's a bug somewhere. + assert True == False, ('Error performing exclusion: ' + 's1: %s s2: %s other: %s' % + (str(s1), str(s2), str(other))) + if s1 == other: + ret_addrs.append(s2) + elif s2 == other: + ret_addrs.append(s1) + else: + # If we got here, there's a bug somewhere. + assert True == False, ('Error performing exclusion: ' + 's1: %s s2: %s other: %s' % + (str(s1), str(s2), str(other))) + + return sorted(ret_addrs, key=_BaseNet._get_networks_key) + + def compare_networks(self, other): + """Compare two IP objects. + + This is only concerned about the comparison of the integer + representation of the network addresses. This means that the + host bits aren't considered at all in this method. If you want + to compare host bits, you can easily enough do a + 'HostA._ip < HostB._ip' + + Args: + other: An IP object. + + Returns: + If the IP versions of self and other are the same, returns: + + -1 if self < other: + eg: IPv4('1.1.1.0/24') < IPv4('1.1.2.0/24') + IPv6('1080::200C:417A') < IPv6('1080::200B:417B') + 0 if self == other + eg: IPv4('1.1.1.1/24') == IPv4('1.1.1.2/24') + IPv6('1080::200C:417A/96') == IPv6('1080::200C:417B/96') + 1 if self > other + eg: IPv4('1.1.1.0/24') > IPv4('1.1.0.0/24') + IPv6('1080::1:200C:417A/112') > + IPv6('1080::0:200C:417A/112') + + If the IP versions of self and other are different, returns: + + -1 if self._version < other._version + eg: IPv4('10.0.0.1/24') < IPv6('::1/128') + 1 if self._version > other._version + eg: IPv6('::1/128') > IPv4('255.255.255.0/24') + + """ + if self._version < other._version: + return -1 + if self._version > other._version: + return 1 + # self._version == other._version below here: + if self.network < other.network: + return -1 + if self.network > other.network: + return 1 + # self.network == other.network below here: + if self.netmask < other.netmask: + return -1 + if self.netmask > other.netmask: + return 1 + # self.network == other.network and self.netmask == other.netmask + return 0 + + def _get_networks_key(self): + """Network-only key function. + + Returns an object that identifies this address' network and + netmask. This function is a suitable "key" argument for sorted() + and list.sort(). + + """ + return (self._version, self.network, self.netmask) + + def _ip_int_from_prefix(self, prefixlen=None): + """Turn the prefix length netmask into a int for comparison. + + Args: + prefixlen: An integer, the prefix length. + + Returns: + An integer. + + """ + if not prefixlen and prefixlen != 0: + prefixlen = self._prefixlen + return self._ALL_ONES ^ (self._ALL_ONES >> prefixlen) + + def _prefix_from_ip_int(self, ip_int, mask=32): + """Return prefix length from the decimal netmask. + + Args: + ip_int: An integer, the IP address. + mask: The netmask. Defaults to 32. + + Returns: + An integer, the prefix length. + + """ + while mask: + if ip_int & 1 == 1: + break + ip_int >>= 1 + mask -= 1 + + return mask + + def _ip_string_from_prefix(self, prefixlen=None): + """Turn a prefix length into a dotted decimal string. + + Args: + prefixlen: An integer, the netmask prefix length. + + Returns: + A string, the dotted decimal netmask string. + + """ + if not prefixlen: + prefixlen = self._prefixlen + return self._string_from_ip_int(self._ip_int_from_prefix(prefixlen)) + + def iter_subnets(self, prefixlen_diff=1, new_prefix=None): + """The subnets which join to make the current subnet. + + In the case that self contains only one IP + (self._prefixlen == 32 for IPv4 or self._prefixlen == 128 + for IPv6), return a list with just ourself. + + Args: + prefixlen_diff: An integer, the amount the prefix length + should be increased by. This should not be set if + new_prefix is also set. + new_prefix: The desired new prefix length. This must be a + larger number (smaller prefix) than the existing prefix. + This should not be set if prefixlen_diff is also set. + + Returns: + An iterator of IPv(4|6) objects. + + Raises: + ValueError: The prefixlen_diff is too small or too large. + OR + prefixlen_diff and new_prefix are both set or new_prefix + is a smaller number than the current prefix (smaller + number means a larger network) + + """ + if self._prefixlen == self._max_prefixlen: + yield self + return + + if new_prefix is not None: + if new_prefix < self._prefixlen: + raise ValueError('new prefix must be longer') + if prefixlen_diff != 1: + raise ValueError('cannot set prefixlen_diff and new_prefix') + prefixlen_diff = new_prefix - self._prefixlen + + if prefixlen_diff < 0: + raise ValueError('prefix length diff must be > 0') + new_prefixlen = self._prefixlen + prefixlen_diff + + if not self._is_valid_netmask(str(new_prefixlen)): + raise ValueError( + 'prefix length diff %d is invalid for netblock %s' % ( + new_prefixlen, str(self))) + + first = IPNetwork('%s/%s' % (str(self.network), + str(self._prefixlen + prefixlen_diff)), + version=self._version) + + yield first + current = first + while True: + broadcast = current.broadcast + if broadcast == self.broadcast: + return + new_addr = IPAddress(int(broadcast) + 1, version=self._version) + current = IPNetwork('%s/%s' % (str(new_addr), str(new_prefixlen)), + version=self._version) + + yield current + + def masked(self): + """Return the network object with the host bits masked out.""" + return IPNetwork('%s/%d' % (self.network, self._prefixlen), + version=self._version) + + def subnet(self, prefixlen_diff=1, new_prefix=None): + """Return a list of subnets, rather than an iterator.""" + return list(self.iter_subnets(prefixlen_diff, new_prefix)) + + def supernet(self, prefixlen_diff=1, new_prefix=None): + """The supernet containing the current network. + + Args: + prefixlen_diff: An integer, the amount the prefix length of + the network should be decreased by. For example, given a + /24 network and a prefixlen_diff of 3, a supernet with a + /21 netmask is returned. + + Returns: + An IPv4 network object. + + Raises: + ValueError: If self.prefixlen - prefixlen_diff < 0. I.e., you have a + negative prefix length. + OR + If prefixlen_diff and new_prefix are both set or new_prefix is a + larger number than the current prefix (larger number means a + smaller network) + + """ + if self._prefixlen == 0: + return self + + if new_prefix is not None: + if new_prefix > self._prefixlen: + raise ValueError('new prefix must be shorter') + if prefixlen_diff != 1: + raise ValueError('cannot set prefixlen_diff and new_prefix') + prefixlen_diff = self._prefixlen - new_prefix + + if self.prefixlen - prefixlen_diff < 0: + raise ValueError( + 'current prefixlen is %d, cannot have a prefixlen_diff of %d' % + (self.prefixlen, prefixlen_diff)) + return IPNetwork('%s/%s' % (str(self.network), + str(self.prefixlen - prefixlen_diff)), + version=self._version) + + # backwards compatibility + Subnet = subnet + Supernet = supernet + AddressExclude = address_exclude + CompareNetworks = compare_networks + Contains = __contains__ + + +class _BaseV4(object): + + """Base IPv4 object. + + The following methods are used by IPv4 objects in both single IP + addresses and networks. + + """ + + # Equivalent to 255.255.255.255 or 32 bits of 1's. + _ALL_ONES = (2 ** IPV4LENGTH) - 1 + _DECIMAL_DIGITS = frozenset('0123456789') + + def __init__(self, address): + self._version = 4 + self._max_prefixlen = IPV4LENGTH + + def _explode_shorthand_ip_string(self): + return str(self) + + def _ip_int_from_string(self, ip_str): + """Turn the given IP string into an integer for comparison. + + Args: + ip_str: A string, the IP ip_str. + + Returns: + The IP ip_str as an integer. + + Raises: + AddressValueError: if ip_str isn't a valid IPv4 Address. + + """ + octets = ip_str.split('.') + if len(octets) != 4: + raise AddressValueError(ip_str) + + packed_ip = 0 + for oc in octets: + try: + packed_ip = (packed_ip << 8) | self._parse_octet(oc) + except ValueError: + raise AddressValueError(ip_str) + return packed_ip + + def _parse_octet(self, octet_str): + """Convert a decimal octet into an integer. + + Args: + octet_str: A string, the number to parse. + + Returns: + The octet as an integer. + + Raises: + ValueError: if the octet isn't strictly a decimal from [0..255]. + + """ + # Whitelist the characters, since int() allows a lot of bizarre stuff. + if not self._DECIMAL_DIGITS.issuperset(octet_str): + raise ValueError + octet_int = int(octet_str, 10) + # Disallow leading zeroes, because no clear standard exists on + # whether these should be interpreted as decimal or octal. + if octet_int > 255 or (octet_str[0] == '0' and len(octet_str) > 1): + raise ValueError + return octet_int + + def _string_from_ip_int(self, ip_int): + """Turns a 32-bit integer into dotted decimal notation. + + Args: + ip_int: An integer, the IP address. + + Returns: + The IP address as a string in dotted decimal notation. + + """ + octets = [] + for _ in xrange(4): + octets.insert(0, str(ip_int & 0xFF)) + ip_int >>= 8 + return '.'.join(octets) + + @property + def max_prefixlen(self): + return self._max_prefixlen + + @property + def packed(self): + """The binary representation of this address.""" + return v4_int_to_packed(self._ip) + + @property + def version(self): + return self._version + + @property + def is_reserved(self): + """Test if the address is otherwise IETF reserved. + + Returns: + A boolean, True if the address is within the + reserved IPv4 Network range. + + """ + return self in IPv4Network('240.0.0.0/4') + + @property + def is_private(self): + """Test if this address is allocated for private networks. + + Returns: + A boolean, True if the address is reserved per RFC 1918. + + """ + return (self in IPv4Network('10.0.0.0/8') or + self in IPv4Network('172.16.0.0/12') or + self in IPv4Network('192.168.0.0/16')) + + @property + def is_multicast(self): + """Test if the address is reserved for multicast use. + + Returns: + A boolean, True if the address is multicast. + See RFC 3171 for details. + + """ + return self in IPv4Network('224.0.0.0/4') + + @property + def is_unspecified(self): + """Test if the address is unspecified. + + Returns: + A boolean, True if this is the unspecified address as defined in + RFC 5735 3. + + """ + return self in IPv4Network('0.0.0.0') + + @property + def is_loopback(self): + """Test if the address is a loopback address. + + Returns: + A boolean, True if the address is a loopback per RFC 3330. + + """ + return self in IPv4Network('127.0.0.0/8') + + @property + def is_link_local(self): + """Test if the address is reserved for link-local. + + Returns: + A boolean, True if the address is link-local per RFC 3927. + + """ + return self in IPv4Network('169.254.0.0/16') + + +class IPv4Address(_BaseV4, _BaseIP): + + """Represent and manipulate single IPv4 Addresses.""" + + def __init__(self, address): + + """ + Args: + address: A string or integer representing the IP + '192.168.1.1' + + Additionally, an integer can be passed, so + IPv4Address('192.168.1.1') == IPv4Address(3232235777). + or, more generally + IPv4Address(int(IPv4Address('192.168.1.1'))) == + IPv4Address('192.168.1.1') + + Raises: + AddressValueError: If ipaddr isn't a valid IPv4 address. + + """ + _BaseV4.__init__(self, address) + + # Efficient constructor from integer. + if isinstance(address, (int, long)): + self._ip = address + if address < 0 or address > self._ALL_ONES: + raise AddressValueError(address) + return + + # Constructing from a packed address + if isinstance(address, Bytes): + try: + self._ip, = struct.unpack('!I', address) + except struct.error: + raise AddressValueError(address) # Wrong length. + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP string. + addr_str = str(address) + self._ip = self._ip_int_from_string(addr_str) + + +class IPv4Network(_BaseV4, _BaseNet): + + """This class represents and manipulates 32-bit IPv4 networks. + + Attributes: [examples for IPv4Network('1.2.3.4/27')] + ._ip: 16909060 + .ip: IPv4Address('1.2.3.4') + .network: IPv4Address('1.2.3.0') + .hostmask: IPv4Address('0.0.0.31') + .broadcast: IPv4Address('1.2.3.31') + .netmask: IPv4Address('255.255.255.224') + .prefixlen: 27 + + """ + + # the valid octets for host and netmasks. only useful for IPv4. + _valid_mask_octets = set((255, 254, 252, 248, 240, 224, 192, 128, 0)) + + def __init__(self, address, strict=False): + """Instantiate a new IPv4 network object. + + Args: + address: A string or integer representing the IP [& network]. + '192.168.1.1/24' + '192.168.1.1/255.255.255.0' + '192.168.1.1/0.0.0.255' + are all functionally the same in IPv4. Similarly, + '192.168.1.1' + '192.168.1.1/255.255.255.255' + '192.168.1.1/32' + are also functionaly equivalent. That is to say, failing to + provide a subnetmask will create an object with a mask of /32. + + If the mask (portion after the / in the argument) is given in + dotted quad form, it is treated as a netmask if it starts with a + non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it + starts with a zero field (e.g. 0.255.255.255 == /8), with the + single exception of an all-zero mask which is treated as a + netmask == /0. If no mask is given, a default of /32 is used. + + Additionally, an integer can be passed, so + IPv4Network('192.168.1.1') == IPv4Network(3232235777). + or, more generally + IPv4Network(int(IPv4Network('192.168.1.1'))) == + IPv4Network('192.168.1.1') + + strict: A boolean. If true, ensure that we have been passed + A true network address, eg, 192.168.1.0/24 and not an + IP address on a network, eg, 192.168.1.1/24. + + Raises: + AddressValueError: If ipaddr isn't a valid IPv4 address. + NetmaskValueError: If the netmask isn't valid for + an IPv4 address. + ValueError: If strict was True and a network address was not + supplied. + + """ + _BaseNet.__init__(self, address) + _BaseV4.__init__(self, address) + + # Constructing from an integer or packed bytes. + if isinstance(address, (int, long, Bytes)): + self.ip = IPv4Address(address) + self._ip = self.ip._ip + self._prefixlen = self._max_prefixlen + self.netmask = IPv4Address(self._ALL_ONES) + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP prefix string. + addr = str(address).split('/') + + if len(addr) > 2: + raise AddressValueError(address) + + self._ip = self._ip_int_from_string(addr[0]) + self.ip = IPv4Address(self._ip) + + if len(addr) == 2: + mask = addr[1].split('.') + if len(mask) == 4: + # We have dotted decimal netmask. + if self._is_valid_netmask(addr[1]): + self.netmask = IPv4Address(self._ip_int_from_string( + addr[1])) + elif self._is_hostmask(addr[1]): + self.netmask = IPv4Address( + self._ip_int_from_string(addr[1]) ^ self._ALL_ONES) + else: + raise NetmaskValueError('%s is not a valid netmask' + % addr[1]) + + self._prefixlen = self._prefix_from_ip_int(int(self.netmask)) + else: + # We have a netmask in prefix length form. + if not self._is_valid_netmask(addr[1]): + raise NetmaskValueError(addr[1]) + self._prefixlen = int(addr[1]) + self.netmask = IPv4Address(self._ip_int_from_prefix( + self._prefixlen)) + else: + self._prefixlen = self._max_prefixlen + self.netmask = IPv4Address(self._ip_int_from_prefix( + self._prefixlen)) + if strict: + if self.ip != self.network: + raise ValueError('%s has host bits set' % + self.ip) + if self._prefixlen == (self._max_prefixlen - 1): + self.iterhosts = self.__iter__ + + def _is_hostmask(self, ip_str): + """Test if the IP string is a hostmask (rather than a netmask). + + Args: + ip_str: A string, the potential hostmask. + + Returns: + A boolean, True if the IP string is a hostmask. + + """ + bits = ip_str.split('.') + try: + parts = [int(x) for x in bits if int(x) in self._valid_mask_octets] + except ValueError: + return False + if len(parts) != len(bits): + return False + if parts[0] < parts[-1]: + return True + return False + + def _is_valid_netmask(self, netmask): + """Verify that the netmask is valid. + + Args: + netmask: A string, either a prefix or dotted decimal + netmask. + + Returns: + A boolean, True if the prefix represents a valid IPv4 + netmask. + + """ + mask = netmask.split('.') + if len(mask) == 4: + if [x for x in mask if int(x) not in self._valid_mask_octets]: + return False + if [y for idx, y in enumerate(mask) if idx > 0 and + y > mask[idx - 1]]: + return False + return True + try: + netmask = int(netmask) + except ValueError: + return False + return 0 <= netmask <= self._max_prefixlen + + # backwards compatibility + IsRFC1918 = lambda self: self.is_private + IsMulticast = lambda self: self.is_multicast + IsLoopback = lambda self: self.is_loopback + IsLinkLocal = lambda self: self.is_link_local + + +class _BaseV6(object): + + """Base IPv6 object. + + The following methods are used by IPv6 objects in both single IP + addresses and networks. + + """ + + _ALL_ONES = (2 ** IPV6LENGTH) - 1 + _HEXTET_COUNT = 8 + _HEX_DIGITS = frozenset('0123456789ABCDEFabcdef') + + def __init__(self, address): + self._version = 6 + self._max_prefixlen = IPV6LENGTH + + def _ip_int_from_string(self, ip_str): + """Turn an IPv6 ip_str into an integer. + + Args: + ip_str: A string, the IPv6 ip_str. + + Returns: + A long, the IPv6 ip_str. + + Raises: + AddressValueError: if ip_str isn't a valid IPv6 Address. + + """ + parts = ip_str.split(':') + + # An IPv6 address needs at least 2 colons (3 parts). + if len(parts) < 3: + raise AddressValueError(ip_str) + + # If the address has an IPv4-style suffix, convert it to hexadecimal. + if '.' in parts[-1]: + ipv4_int = IPv4Address(parts.pop())._ip + parts.append('%x' % ((ipv4_int >> 16) & 0xFFFF)) + parts.append('%x' % (ipv4_int & 0xFFFF)) + + # An IPv6 address can't have more than 8 colons (9 parts). + if len(parts) > self._HEXTET_COUNT + 1: + raise AddressValueError(ip_str) + + # Disregarding the endpoints, find '::' with nothing in between. + # This indicates that a run of zeroes has been skipped. + try: + skip_index, = ( + [i for i in xrange(1, len(parts) - 1) if not parts[i]] or + [None]) + except ValueError: + # Can't have more than one '::' + raise AddressValueError(ip_str) + + # parts_hi is the number of parts to copy from above/before the '::' + # parts_lo is the number of parts to copy from below/after the '::' + if skip_index is not None: + # If we found a '::', then check if it also covers the endpoints. + parts_hi = skip_index + parts_lo = len(parts) - skip_index - 1 + if not parts[0]: + parts_hi -= 1 + if parts_hi: + raise AddressValueError(ip_str) # ^: requires ^:: + if not parts[-1]: + parts_lo -= 1 + if parts_lo: + raise AddressValueError(ip_str) # :$ requires ::$ + parts_skipped = self._HEXTET_COUNT - (parts_hi + parts_lo) + if parts_skipped < 1: + raise AddressValueError(ip_str) + else: + # Otherwise, allocate the entire address to parts_hi. The endpoints + # could still be empty, but _parse_hextet() will check for that. + if len(parts) != self._HEXTET_COUNT: + raise AddressValueError(ip_str) + parts_hi = len(parts) + parts_lo = 0 + parts_skipped = 0 + + try: + # Now, parse the hextets into a 128-bit integer. + ip_int = 0L + for i in xrange(parts_hi): + ip_int <<= 16 + ip_int |= self._parse_hextet(parts[i]) + ip_int <<= 16 * parts_skipped + for i in xrange(-parts_lo, 0): + ip_int <<= 16 + ip_int |= self._parse_hextet(parts[i]) + return ip_int + except ValueError: + raise AddressValueError(ip_str) + + def _parse_hextet(self, hextet_str): + """Convert an IPv6 hextet string into an integer. + + Args: + hextet_str: A string, the number to parse. + + Returns: + The hextet as an integer. + + Raises: + ValueError: if the input isn't strictly a hex number from [0..FFFF]. + + """ + # Whitelist the characters, since int() allows a lot of bizarre stuff. + if not self._HEX_DIGITS.issuperset(hextet_str): + raise ValueError + if len(hextet_str) > 4: + raise ValueError + hextet_int = int(hextet_str, 16) + if hextet_int > 0xFFFF: + raise ValueError + return hextet_int + + def _compress_hextets(self, hextets): + """Compresses a list of hextets. + + Compresses a list of strings, replacing the longest continuous + sequence of "0" in the list with "" and adding empty strings at + the beginning or at the end of the string such that subsequently + calling ":".join(hextets) will produce the compressed version of + the IPv6 address. + + Args: + hextets: A list of strings, the hextets to compress. + + Returns: + A list of strings. + + """ + best_doublecolon_start = -1 + best_doublecolon_len = 0 + doublecolon_start = -1 + doublecolon_len = 0 + for index in range(len(hextets)): + if hextets[index] == '0': + doublecolon_len += 1 + if doublecolon_start == -1: + # Start of a sequence of zeros. + doublecolon_start = index + if doublecolon_len > best_doublecolon_len: + # This is the longest sequence of zeros so far. + best_doublecolon_len = doublecolon_len + best_doublecolon_start = doublecolon_start + else: + doublecolon_len = 0 + doublecolon_start = -1 + + if best_doublecolon_len > 1: + best_doublecolon_end = (best_doublecolon_start + + best_doublecolon_len) + # For zeros at the end of the address. + if best_doublecolon_end == len(hextets): + hextets += [''] + hextets[best_doublecolon_start:best_doublecolon_end] = [''] + # For zeros at the beginning of the address. + if best_doublecolon_start == 0: + hextets = [''] + hextets + + return hextets + + def _string_from_ip_int(self, ip_int=None): + """Turns a 128-bit integer into hexadecimal notation. + + Args: + ip_int: An integer, the IP address. + + Returns: + A string, the hexadecimal representation of the address. + + Raises: + ValueError: The address is bigger than 128 bits of all ones. + + """ + if not ip_int and ip_int != 0: + ip_int = int(self._ip) + + if ip_int > self._ALL_ONES: + raise ValueError('IPv6 address is too large') + + hex_str = '%032x' % ip_int + hextets = [] + for x in range(0, 32, 4): + hextets.append('%x' % int(hex_str[x:x + 4], 16)) + + hextets = self._compress_hextets(hextets) + return ':'.join(hextets) + + def _explode_shorthand_ip_string(self): + """Expand a shortened IPv6 address. + + Args: + ip_str: A string, the IPv6 address. + + Returns: + A string, the expanded IPv6 address. + + """ + if isinstance(self, _BaseNet): + ip_str = str(self.ip) + else: + ip_str = str(self) + + ip_int = self._ip_int_from_string(ip_str) + parts = [] + for i in xrange(self._HEXTET_COUNT): + parts.append('%04x' % (ip_int & 0xFFFF)) + ip_int >>= 16 + parts.reverse() + if isinstance(self, _BaseNet): + return '%s/%d' % (':'.join(parts), self.prefixlen) + return ':'.join(parts) + + @property + def max_prefixlen(self): + return self._max_prefixlen + + @property + def packed(self): + """The binary representation of this address.""" + return v6_int_to_packed(self._ip) + + @property + def version(self): + return self._version + + @property + def is_multicast(self): + """Test if the address is reserved for multicast use. + + Returns: + A boolean, True if the address is a multicast address. + See RFC 2373 2.7 for details. + + """ + return self in IPv6Network('ff00::/8') + + @property + def is_reserved(self): + """Test if the address is otherwise IETF reserved. + + Returns: + A boolean, True if the address is within one of the + reserved IPv6 Network ranges. + + """ + return (self in IPv6Network('::/8') or + self in IPv6Network('100::/8') or + self in IPv6Network('200::/7') or + self in IPv6Network('400::/6') or + self in IPv6Network('800::/5') or + self in IPv6Network('1000::/4') or + self in IPv6Network('4000::/3') or + self in IPv6Network('6000::/3') or + self in IPv6Network('8000::/3') or + self in IPv6Network('A000::/3') or + self in IPv6Network('C000::/3') or + self in IPv6Network('E000::/4') or + self in IPv6Network('F000::/5') or + self in IPv6Network('F800::/6') or + self in IPv6Network('FE00::/9')) + + @property + def is_unspecified(self): + """Test if the address is unspecified. + + Returns: + A boolean, True if this is the unspecified address as defined in + RFC 2373 2.5.2. + + """ + return self._ip == 0 and getattr(self, '_prefixlen', 128) == 128 + + @property + def is_loopback(self): + """Test if the address is a loopback address. + + Returns: + A boolean, True if the address is a loopback address as defined in + RFC 2373 2.5.3. + + """ + return self._ip == 1 and getattr(self, '_prefixlen', 128) == 128 + + @property + def is_link_local(self): + """Test if the address is reserved for link-local. + + Returns: + A boolean, True if the address is reserved per RFC 4291. + + """ + return self in IPv6Network('fe80::/10') + + @property + def is_site_local(self): + """Test if the address is reserved for site-local. + + Note that the site-local address space has been deprecated by RFC 3879. + Use is_private to test if this address is in the space of unique local + addresses as defined by RFC 4193. + + Returns: + A boolean, True if the address is reserved per RFC 3513 2.5.6. + + """ + return self in IPv6Network('fec0::/10') + + @property + def is_private(self): + """Test if this address is allocated for private networks. + + Returns: + A boolean, True if the address is reserved per RFC 4193. + + """ + return self in IPv6Network('fc00::/7') + + @property + def ipv4_mapped(self): + """Return the IPv4 mapped address. + + Returns: + If the IPv6 address is a v4 mapped address, return the + IPv4 mapped address. Return None otherwise. + + """ + if (self._ip >> 32) != 0xFFFF: + return None + return IPv4Address(self._ip & 0xFFFFFFFF) + + @property + def teredo(self): + """Tuple of embedded teredo IPs. + + Returns: + Tuple of the (server, client) IPs or None if the address + doesn't appear to be a teredo address (doesn't start with + 2001::/32) + + """ + if (self._ip >> 96) != 0x20010000: + return None + return (IPv4Address((self._ip >> 64) & 0xFFFFFFFF), + IPv4Address(~self._ip & 0xFFFFFFFF)) + + @property + def sixtofour(self): + """Return the IPv4 6to4 embedded address. + + Returns: + The IPv4 6to4-embedded address if present or None if the + address doesn't appear to contain a 6to4 embedded address. + + """ + if (self._ip >> 112) != 0x2002: + return None + return IPv4Address((self._ip >> 80) & 0xFFFFFFFF) + + +class IPv6Address(_BaseV6, _BaseIP): + + """Represent and manipulate single IPv6 Addresses. + """ + + def __init__(self, address): + """Instantiate a new IPv6 address object. + + Args: + address: A string or integer representing the IP + + Additionally, an integer can be passed, so + IPv6Address('2001:4860::') == + IPv6Address(42541956101370907050197289607612071936L). + or, more generally + IPv6Address(IPv6Address('2001:4860::')._ip) == + IPv6Address('2001:4860::') + + Raises: + AddressValueError: If address isn't a valid IPv6 address. + + """ + _BaseV6.__init__(self, address) + + # Efficient constructor from integer. + if isinstance(address, (int, long)): + self._ip = address + if address < 0 or address > self._ALL_ONES: + raise AddressValueError(address) + return + + # Constructing from a packed address + if isinstance(address, Bytes): + try: + hi, lo = struct.unpack('!QQ', address) + except struct.error: + raise AddressValueError(address) # Wrong length. + self._ip = (hi << 64) | lo + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP string. + addr_str = str(address) + if not addr_str: + raise AddressValueError('') + + self._ip = self._ip_int_from_string(addr_str) + + +class IPv6Network(_BaseV6, _BaseNet): + + """This class represents and manipulates 128-bit IPv6 networks. + + Attributes: [examples for IPv6('2001:658:22A:CAFE:200::1/64')] + .ip: IPv6Address('2001:658:22a:cafe:200::1') + .network: IPv6Address('2001:658:22a:cafe::') + .hostmask: IPv6Address('::ffff:ffff:ffff:ffff') + .broadcast: IPv6Address('2001:658:22a:cafe:ffff:ffff:ffff:ffff') + .netmask: IPv6Address('ffff:ffff:ffff:ffff::') + .prefixlen: 64 + + """ + + def __init__(self, address, strict=False): + """Instantiate a new IPv6 Network object. + + Args: + address: A string or integer representing the IPv6 network or the IP + and prefix/netmask. + '2001:4860::/128' + '2001:4860:0000:0000:0000:0000:0000:0000/128' + '2001:4860::' + are all functionally the same in IPv6. That is to say, + failing to provide a subnetmask will create an object with + a mask of /128. + + Additionally, an integer can be passed, so + IPv6Network('2001:4860::') == + IPv6Network(42541956101370907050197289607612071936L). + or, more generally + IPv6Network(IPv6Network('2001:4860::')._ip) == + IPv6Network('2001:4860::') + + strict: A boolean. If true, ensure that we have been passed + A true network address, eg, 192.168.1.0/24 and not an + IP address on a network, eg, 192.168.1.1/24. + + Raises: + AddressValueError: If address isn't a valid IPv6 address. + NetmaskValueError: If the netmask isn't valid for + an IPv6 address. + ValueError: If strict was True and a network address was not + supplied. + + """ + _BaseNet.__init__(self, address) + _BaseV6.__init__(self, address) + + # Constructing from an integer or packed bytes. + if isinstance(address, (int, long, Bytes)): + self.ip = IPv6Address(address) + self._ip = self.ip._ip + self._prefixlen = self._max_prefixlen + self.netmask = IPv6Address(self._ALL_ONES) + return + + # Assume input argument to be string or any object representation + # which converts into a formatted IP prefix string. + addr = str(address).split('/') + + if len(addr) > 2: + raise AddressValueError(address) + + self._ip = self._ip_int_from_string(addr[0]) + self.ip = IPv6Address(self._ip) + + if len(addr) == 2: + if self._is_valid_netmask(addr[1]): + self._prefixlen = int(addr[1]) + else: + raise NetmaskValueError(addr[1]) + else: + self._prefixlen = self._max_prefixlen + + self.netmask = IPv6Address(self._ip_int_from_prefix(self._prefixlen)) + + if strict: + if self.ip != self.network: + raise ValueError('%s has host bits set' % + self.ip) + if self._prefixlen == (self._max_prefixlen - 1): + self.iterhosts = self.__iter__ + + def _is_valid_netmask(self, prefixlen): + """Verify that the netmask/prefixlen is valid. + + Args: + prefixlen: A string, the netmask in prefix length format. + + Returns: + A boolean, True if the prefix represents a valid IPv6 + netmask. + + """ + try: + prefixlen = int(prefixlen) + except ValueError: + return False + return 0 <= prefixlen <= self._max_prefixlen + + @property + def with_netmask(self): + return self.with_prefixlen diff --git a/rhodecode/lib/markup_renderer.py b/rhodecode/lib/markup_renderer.py --- a/rhodecode/lib/markup_renderer.py +++ b/rhodecode/lib/markup_renderer.py @@ -98,7 +98,7 @@ class MarkupRenderer(object): source = safe_unicode(source) try: import markdown as __markdown - return __markdown.markdown(source, ['codehilite', 'tables']) + return __markdown.markdown(source, ['codehilite', 'extra']) except ImportError: log.warning('Install markdown to use this function') return cls.plain(source) diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py +++ b/rhodecode/lib/middleware/simplegit.py @@ -109,7 +109,7 @@ class SimpleGit(BaseVCSController): if not self._check_ssl(environ, start_response): return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response) - ipaddr = self._get_ip_addr(environ) + ip_addr = self._get_ip_addr(environ) username = None self._git_first_op = False # skip passing error to error controller @@ -140,7 +140,7 @@ class SimpleGit(BaseVCSController): anonymous_user = self.__get_user('default') username = anonymous_user.username anonymous_perm = self._check_permission(action, anonymous_user, - repo_name) + repo_name, ip_addr) if anonymous_perm is not True or anonymous_user.active is False: if anonymous_perm is not True: @@ -182,7 +182,7 @@ class SimpleGit(BaseVCSController): return HTTPInternalServerError()(environ, start_response) #check permissions for this repository - perm = self._check_permission(action, user, repo_name) + perm = self._check_permission(action, user, repo_name, ip_addr) if perm is not True: return HTTPForbidden()(environ, start_response) @@ -191,7 +191,7 @@ class SimpleGit(BaseVCSController): from rhodecode import CONFIG server_url = get_server_url(environ) extras = { - 'ip': ipaddr, + 'ip': ip_addr, 'username': username, 'action': action, 'repository': repo_name, @@ -233,11 +233,12 @@ class SimpleGit(BaseVCSController): self._invalidate_cache(repo_name) self._handle_githooks(repo_name, action, baseui, environ) - log.info('%s action on GIT repo "%s"' % (action, repo_name)) + log.info('%s action on GIT repo "%s" by "%s" from %s' % + (action, repo_name, username, ip_addr)) app = self.__make_app(repo_name, repo_path, extras) return app(environ, start_response) except HTTPLockedRC, e: - log.debug('Repositry LOCKED ret code 423!') + log.debug('Repository LOCKED ret code 423!') return e(environ, start_response) except Exception: log.error(traceback.format_exc()) diff --git a/rhodecode/lib/middleware/simplehg.py b/rhodecode/lib/middleware/simplehg.py --- a/rhodecode/lib/middleware/simplehg.py +++ b/rhodecode/lib/middleware/simplehg.py @@ -73,7 +73,7 @@ class SimpleHg(BaseVCSController): if not self._check_ssl(environ, start_response): return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response) - ipaddr = self._get_ip_addr(environ) + ip_addr = self._get_ip_addr(environ) username = None # skip passing error to error controller environ['pylons.status_code_redirect'] = True @@ -103,7 +103,7 @@ class SimpleHg(BaseVCSController): anonymous_user = self.__get_user('default') username = anonymous_user.username anonymous_perm = self._check_permission(action, anonymous_user, - repo_name) + repo_name, ip_addr) if anonymous_perm is not True or anonymous_user.active is False: if anonymous_perm is not True: @@ -145,7 +145,7 @@ class SimpleHg(BaseVCSController): return HTTPInternalServerError()(environ, start_response) #check permissions for this repository - perm = self._check_permission(action, user, repo_name) + perm = self._check_permission(action, user, repo_name, ip_addr) if perm is not True: return HTTPForbidden()(environ, start_response) @@ -154,7 +154,7 @@ class SimpleHg(BaseVCSController): from rhodecode import CONFIG server_url = get_server_url(environ) extras = { - 'ip': ipaddr, + 'ip': ip_addr, 'username': username, 'action': action, 'repository': repo_name, @@ -194,14 +194,15 @@ class SimpleHg(BaseVCSController): # invalidate cache on push if action == 'push': self._invalidate_cache(repo_name) - log.info('%s action on HG repo "%s"' % (action, repo_name)) + log.info('%s action on HG repo "%s" by "%s" from %s' % + (action, repo_name, username, ip_addr)) app = self.__make_app(repo_path, baseui, extras) return app(environ, start_response) except RepoError, e: if str(e).find('not found') != -1: return HTTPNotFound()(environ, start_response) except HTTPLockedRC, e: - log.debug('Repositry LOCKED ret code 423!') + log.debug('Repository LOCKED ret code 423!') return e(environ, start_response) except Exception: log.error(traceback.format_exc()) diff --git a/rhodecode/lib/update_repoinfo.py b/rhodecode/lib/update_repoinfo.py --- a/rhodecode/lib/update_repoinfo.py +++ b/rhodecode/lib/update_repoinfo.py @@ -34,6 +34,7 @@ from os.path import dirname as dn, join from rhodecode.model import init_model from rhodecode.lib.utils2 import engine_from_config, safe_str from rhodecode.model.db import RhodeCodeUi, Repository +from rhodecode.lib.vcs.backends.base import EmptyChangeset #to get the rhodecode import @@ -73,8 +74,9 @@ class UpdateCommand(BasePasterCommand): else: repo_list = Repository.getAll() for repo in repo_list: - last_change = repo.scm_instance.last_change - repo.update_last_change(last_change) + last_cs = (repo.scm_instance.get_changeset() if repo.scm_instance + else EmptyChangeset()) + repo.update_changeset_cache(last_cs) def update_parser(self): self.parser.add_option('--update-only', diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -162,10 +162,8 @@ def action_logger(user, action, repo, ip user_log.user_ip = ipaddr sa.add(user_log) - log.info( - 'Adding user %s, action %s on %s' % (user_obj, action, - safe_unicode(repo)) - ) + log.info('Logging action %s on %s by %s' % + (action, safe_unicode(repo), user_obj)) if commit: sa.commit() except: @@ -309,7 +307,7 @@ def make_ui(read_from='file', path=None, cfg.read(path) for section in ui_sections: for k, v in cfg.items(section): - log.debug('settings ui from file[%s]%s:%s' % (section, k, v)) + log.debug('settings ui from file: [%s] %s=%s' % (section, k, v)) baseui.setconfig(safe_str(section), safe_str(k), safe_str(v)) elif read_from == 'db': @@ -321,7 +319,7 @@ def make_ui(read_from='file', path=None, hg_ui = ret for ui_ in hg_ui: if ui_.ui_active: - log.debug('settings ui from db[%s]%s:%s', ui_.ui_section, + log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section, ui_.ui_key, ui_.ui_value) baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key), safe_str(ui_.ui_value)) @@ -423,6 +421,13 @@ def repo2db_mapper(initial_repo_list, re # CacheInvalidation.clear_cache() # sa.commit() + ##creation defaults + defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True) + enable_statistics = defs.get('repo_enable_statistics') + enable_locking = defs.get('repo_enable_locking') + enable_downloads = defs.get('repo_enable_downloads') + private = defs.get('repo_private') + for name, repo in initial_repo_list.items(): group = map_groups(name) db_repo = rm.get_by_repo_name(name) @@ -433,18 +438,24 @@ def repo2db_mapper(initial_repo_list, re desc = (repo.description if repo.description != 'unknown' else '%s repository' % name) + new_repo = rm.create_repo( repo_name=name, repo_type=repo.alias, description=desc, repos_group=getattr(group, 'group_id', None), owner=user, - just_db=True + just_db=True, + enable_locking=enable_locking, + enable_downloads=enable_downloads, + enable_statistics=enable_statistics, + private=private ) # we added that repo just now, and make sure it has githook # installed if new_repo.repo_type == 'git': ScmModel().install_git_hook(new_repo.scm_instance) + new_repo.update_changeset_cache() elif install_git_hook: if db_repo.repo_type == 'git': ScmModel().install_git_hook(db_repo.scm_instance) @@ -452,8 +463,8 @@ def repo2db_mapper(initial_repo_list, re # system, this will register all repos and multiple instances key, _prefix, _org_key = CacheInvalidation._get_key(name) CacheInvalidation.invalidate(name) - log.debug("Creating a cache key for %s instance_id=>`%s`" - % (name, _prefix or '-')) + log.debug("Creating a cache key for %s, instance_id %s" + % (name, _prefix or 'unknown')) sa.commit() removed = [] @@ -740,4 +751,4 @@ def jsonify(func, *args, **kwargs): warnings.warn(msg, Warning, 2) log.warning(msg) log.debug("Returning JSON wrapped action output") - return json.dumps(data, encoding='utf-8') \ No newline at end of file + return json.dumps(data, encoding='utf-8') diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -376,6 +376,7 @@ class BaseChangeset(object): return dict( short_id=self.short_id, raw_id=self.raw_id, + revision=self.revision, message=self.message, date=self.date, author=self.author, diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -606,10 +606,13 @@ class GitRepository(BaseRepository): Tries to pull changes from external location. """ url = self._get_url(url) - cmd = ['fetch'] - cmd.append(url) - cmd = ' '.join(cmd) - # If error occurs run_git_command raises RepositoryError already + so, se = self.run_git_command('ls-remote -h %s' % url) + refs = [] + for line in (x for x in so.splitlines()): + sha, ref = line.split('\t') + refs.append(ref) + refs = ' '.join(('+%s:%s' % (r, r) for r in refs)) + cmd = '''fetch %s -- %s''' % (url, refs) self.run_git_command(cmd) @LazyProperty diff --git a/rhodecode/lib/vcs/nodes.py b/rhodecode/lib/vcs/nodes.py --- a/rhodecode/lib/vcs/nodes.py +++ b/rhodecode/lib/vcs/nodes.py @@ -362,10 +362,11 @@ class FileNode(Node): Returns pygment's lexer class. Would try to guess lexer taking file's content, name and mimetype. """ + try: - lexer = lexers.guess_lexer_for_filename(self.name, self.content) + lexer = lexers.guess_lexer_for_filename(self.name, self.content, stripnl=False) except lexers.ClassNotFound: - lexer = lexers.TextLexer() + lexer = lexers.TextLexer(stripnl=False) # returns first alias return lexer diff --git a/rhodecode/model/changeset_status.py b/rhodecode/model/changeset_status.py --- a/rhodecode/model/changeset_status.py +++ b/rhodecode/model/changeset_status.py @@ -89,44 +89,39 @@ class ChangesetStatusModel(BaseModel): with_revisions) return q.all() - def get_status(self, repo, revision=None, pull_request=None): + def get_status(self, repo, revision=None, pull_request=None, as_str=True): """ Returns latest status of changeset for given revision or for given pull request. Statuses are versioned inside a table itself and version == 0 is always the current one :param repo: - :type repo: :param revision: 40char hash or None - :type revision: str :param pull_request: pull_request reference - :type: + :param as_str: return status as string not object """ q = self._get_status_query(repo, revision, pull_request) # need to use first here since there can be multiple statuses # returned from pull_request status = q.first() - status = status.status if status else status - st = status or ChangesetStatus.DEFAULT - return str(st) + if as_str: + status = status.status if status else status + st = status or ChangesetStatus.DEFAULT + return str(st) + return status - def set_status(self, repo, status, user, comment, revision=None, + def set_status(self, repo, status, user, comment=None, revision=None, pull_request=None, dont_allow_on_closed_pull_request=False): """ Creates new status for changeset or updates the old ones bumping their version, leaving the current status at :param repo: - :type repo: :param revision: - :type revision: :param status: - :type status: :param user: - :type user: :param comment: - :type comment: :param dont_allow_on_closed_pull_request: don't allow a status change if last status was for pull request and it's closed. We shouldn't mess around this manually @@ -134,14 +129,21 @@ class ChangesetStatusModel(BaseModel): repo = self._get_repo(repo) q = ChangesetStatus.query() - + if not comment: + from rhodecode.model.comment import ChangesetCommentsModel + comment = ChangesetCommentsModel().create( + text='Auto status change', + repo=repo, + user=user, + pull_request=pull_request, + ) if revision: q = q.filter(ChangesetStatus.repo == repo) q = q.filter(ChangesetStatus.revision == revision) elif pull_request: pull_request = self.__get_pull_request(pull_request) q = q.filter(ChangesetStatus.repo == pull_request.org_repo) - q = q.filter(ChangesetStatus.pull_request == pull_request) + q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions)) cur_statuses = q.all() #if statuses exists and last is associated with a closed pull request @@ -153,6 +155,7 @@ class ChangesetStatusModel(BaseModel): 'Changing status on closed pull request is not allowed' ) + #update all current statuses with older version if cur_statuses: for st in cur_statuses: st.version += 1 diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -370,6 +370,11 @@ class User(Base, BaseModel): return [self.email] + [x.email for x in other] @property + def ip_addresses(self): + ret = UserIpMap.query().filter(UserIpMap.user == self).all() + return [x.ip_addr for x in ret] + + @property def username_and_name(self): return '%s (%s %s)' % (self.username, self.firstname, self.lastname) @@ -472,6 +477,7 @@ class User(Base, BaseModel): admin=user.admin, ldap_dn=user.ldap_dn, last_login=user.last_login, + ip_addresses=user.ip_addresses ) return data @@ -518,6 +524,34 @@ class UserEmailMap(Base, BaseModel): self._email = val.lower() if val else None +class UserIpMap(Base, BaseModel): + __tablename__ = 'user_ip_map' + __table_args__ = ( + UniqueConstraint('user_id', 'ip_addr'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'} + ) + __mapper_args__ = {} + + ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=True) + user = relationship('User', lazy='joined') + + @classmethod + def _get_ip_range(cls, ip_addr): + from rhodecode.lib import ipaddr + net = ipaddr.IPv4Network(ip_addr) + return [str(net.network), str(net.broadcast)] + + def __json__(self): + return dict( + ip_addr=self.ip_addr, + ip_range=self._get_ip_range(self.ip_addr) + ) + + class UserLog(Base, BaseModel): __tablename__ = 'user_logs' __table_args__ = ( @@ -637,6 +671,7 @@ class Repository(Base, BaseModel): landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None) enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) + _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None) @@ -682,11 +717,40 @@ class Repository(Base, BaseModel): else: self._locked = None + @hybrid_property + def changeset_cache(self): + from rhodecode.lib.vcs.backends.base import EmptyChangeset + dummy = EmptyChangeset().__json__() + if not self._changeset_cache: + return dummy + try: + return json.loads(self._changeset_cache) + except TypeError: + return dummy + + @changeset_cache.setter + def changeset_cache(self, val): + try: + self._changeset_cache = json.dumps(val) + except: + log.error(traceback.format_exc()) + @classmethod def url_sep(cls): return URL_SEP @classmethod + def normalize_repo_name(cls, repo_name): + """ + Normalizes os specific repo_name to the format internally stored inside + dabatabase using URL_SEP + + :param cls: + :param repo_name: + """ + return cls.url_sep().join(repo_name.split(os.sep)) + + @classmethod def get_by_repo_name(cls, repo_name): q = Session().query(cls).filter(cls.repo_name == repo_name) q = q.options(joinedload(Repository.fork))\ @@ -697,6 +761,7 @@ class Repository(Base, BaseModel): @classmethod def get_by_full_path(cls, repo_full_path): repo_name = repo_full_path.split(cls.base_path(), 1)[-1] + repo_name = cls.normalize_repo_name(repo_name) return cls.get_by_repo_name(repo_name.strip(URL_SEP)) @classmethod @@ -841,7 +906,11 @@ class Repository(Base, BaseModel): description=repo.description, landing_rev=repo.landing_rev, owner=repo.user.username, - fork_of=repo.fork.repo_name if repo.fork else None + fork_of=repo.fork.repo_name if repo.fork else None, + enable_statistics=repo.enable_statistics, + enable_locking=repo.enable_locking, + enable_downloads=repo.enable_downloads, + last_changeset=repo.changeset_cache ) return data @@ -862,6 +931,25 @@ class Repository(Base, BaseModel): def last_db_change(self): return self.updated_on + def clone_url(self, **override): + from pylons import url + from urlparse import urlparse + import urllib + parsed_url = urlparse(url('home', qualified=True)) + default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s' + decoded_path = safe_unicode(urllib.unquote(parsed_url.path)) + args = { + 'user': '', + 'pass': '', + 'scheme': parsed_url.scheme, + 'netloc': parsed_url.netloc, + 'prefix': decoded_path, + 'path': self.repo_name + } + + args.update(override) + return default_clone_uri % args + #========================================================================== # SCM PROPERTIES #========================================================================== @@ -876,12 +964,30 @@ class Repository(Base, BaseModel): cs = self.get_changeset(self.landing_rev) or self.get_changeset() return cs - def update_last_change(self, last_change=None): - if last_change is None: - last_change = datetime.datetime.now() - if self.updated_on is None or self.updated_on != last_change: - log.debug('updated repo %s with new date %s' % (self, last_change)) + def update_changeset_cache(self, cs_cache=None): + """ + Update cache of last changeset for repository, keys should be:: + + short_id + raw_id + revision + message + date + author + + :param cs_cache: + """ + from rhodecode.lib.vcs.backends.base import BaseChangeset + if cs_cache is None: + cs_cache = self.get_changeset() + if isinstance(cs_cache, BaseChangeset): + cs_cache = cs_cache.__json__() + + if cs_cache != self.changeset_cache: + last_change = cs_cache.get('date') or self.last_change + log.debug('updated repo %s with new cs cache %s' % (self, cs_cache)) self.updated_on = last_change + self.changeset_cache = cs_cache Session().add(self) Session().commit() @@ -1708,6 +1814,14 @@ class PullRequest(Base, BaseModel): def revisions(self, val): self._revisions = ':'.join(val) + @property + def org_ref_parts(self): + return self.org_ref.split(':') + + @property + def other_ref_parts(self): + return self.other_ref.split(':') + author = relationship('User', lazy='joined') reviewers = relationship('PullRequestReviewers', cascade="all, delete, delete-orphan") diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -345,9 +345,14 @@ def LdapSettingsForm(tls_reqcert_choices def UserExtraEmailForm(): class _UserExtraEmailForm(formencode.Schema): - email = All(v.UniqSystemEmail(), v.Email) + email = All(v.UniqSystemEmail(), v.Email(not_empty=True)) + return _UserExtraEmailForm + - return _UserExtraEmailForm +def UserExtraIpForm(): + class _UserExtraIpForm(formencode.Schema): + ip = v.ValidIp()(not_empty=True) + return _UserExtraIpForm def PullRequestForm(repo_id): @@ -360,7 +365,8 @@ def PullRequestForm(repo_id): org_ref = v.UnicodeString(strip=True, required=True) other_repo = v.UnicodeString(strip=True, required=True) other_ref = v.UnicodeString(strip=True, required=True) - revisions = All(v.NotReviewedRevisions(repo_id)(), v.UniqueList(not_empty=True)) + revisions = All(#v.NotReviewedRevisions(repo_id)(), + v.UniqueList(not_empty=True)) review_members = v.UniqueList(not_empty=True) pullrequest_title = v.UnicodeString(strip=True, required=True, min=3) diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -270,8 +270,9 @@ class EmailNotificationModel(BaseModel): base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT]) email_template = self._tmpl_lookup.get_template(base) - # translator inject - _kwargs = {'_': _} + # translator and helpers inject + _kwargs = {'_': _, + 'h': h} _kwargs.update(kwargs) log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs)) return email_template.render(**_kwargs) diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -33,7 +33,8 @@ from pylons.i18n.translation import _ from rhodecode.model.meta import Session from rhodecode.lib import helpers as h from rhodecode.model import BaseModel -from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification +from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification,\ + ChangesetStatus from rhodecode.model.notification import NotificationModel from rhodecode.lib.utils2 import safe_unicode @@ -54,8 +55,9 @@ class PullRequestModel(BaseModel): repo = self._get_repo(repo) return PullRequest.query().filter(PullRequest.other_repo == repo).all() - def create(self, created_by, org_repo, org_ref, other_repo, - other_ref, revisions, reviewers, title, description=None): + def create(self, created_by, org_repo, org_ref, other_repo, other_ref, + revisions, reviewers, title, description=None): + from rhodecode.model.changeset_status import ChangesetStatusModel created_by_user = self._get_user(created_by) org_repo = self._get_repo(org_repo) @@ -78,6 +80,14 @@ class PullRequestModel(BaseModel): reviewer = PullRequestReviewers(_usr, new) self.sa.add(reviewer) + #reset state to under-review + ChangesetStatusModel().set_status( + repo=org_repo, + status=ChangesetStatus.STATUS_UNDER_REVIEW, + user=created_by_user, + pull_request=new + ) + #notification to reviewers notif = NotificationModel() diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -41,6 +41,7 @@ from rhodecode.model.db import Repositor Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, RepoGroup,\ RhodeCodeSetting from rhodecode.lib import helpers as h +from rhodecode.lib.auth import HasRepoPermissionAny log = logging.getLogger(__name__) @@ -89,6 +90,22 @@ class RepoModel(BaseModel): "get_repo_%s" % repo_name)) return repo.scalar() + def get_all_user_repos(self, user): + """ + Get's all repositories that user have at least read access + + :param user: + :type user: + """ + from rhodecode.lib.auth import AuthUser + user = self._get_user(user) + repos = AuthUser(user_id=user.user_id).permissions['repositories'] + access_check = lambda r: r[1] in ['repository.read', + 'repository.write', + 'repository.admin'] + repos = [x[0] for x in filter(access_check, repos.items())] + return Repository.query().filter(Repository.repo_name.in_(repos)) + def get_users_js(self): users = self.sa.query(User).filter(User.active == True).all() return json.dumps([ @@ -113,6 +130,95 @@ class RepoModel(BaseModel): } for gr in users_groups] ) + @classmethod + def _render_datatable(cls, tmpl, *args, **kwargs): + import rhodecode + from pylons import tmpl_context as c + from pylons.i18n.translation import _ + + _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup + template = _tmpl_lookup.get_template('data_table/_dt_elements.html') + + tmpl = template.get_def(tmpl) + kwargs.update(dict(_=_, h=h, c=c)) + return tmpl.render(*args, **kwargs) + + def get_repos_as_dict(self, repos_list=None, admin=False, perm_check=True): + _render = self._render_datatable + + def quick_menu(repo_name): + return _render('quick_menu', repo_name) + + def repo_lnk(name, rtype, private, fork_of): + return _render('repo_name', name, rtype, private, fork_of, + short_name=not admin, admin=False) + + def last_change(last_change): + return _render("last_change", last_change) + + def rss_lnk(repo_name): + return _render("rss", repo_name) + + def atom_lnk(repo_name): + return _render("atom", repo_name) + + def last_rev(repo_name, cs_cache): + return _render('revision', repo_name, cs_cache.get('revision'), + cs_cache.get('raw_id'), cs_cache.get('author'), + cs_cache.get('message')) + + def desc(desc): + from pylons import tmpl_context as c + if c.visual.stylify_metatags: + return h.urlify_text(h.desc_stylize(h.truncate(desc, 60))) + else: + return h.urlify_text(h.truncate(desc, 60)) + + def repo_actions(repo_name): + return _render('repo_actions', repo_name) + + def owner_actions(user_id, username): + return _render('user_name', user_id, username) + + repos_data = [] + for repo in repos_list: + if perm_check: + # check permission at this level + if not HasRepoPermissionAny( + 'repository.read', 'repository.write', 'repository.admin' + )(repo.repo_name, 'get_repos_as_dict check'): + continue + cs_cache = repo.changeset_cache + row = { + "menu": quick_menu(repo.repo_name), + "raw_name": repo.repo_name.lower(), + "name": repo_lnk(repo.repo_name, repo.repo_type, + repo.private, repo.fork), + "last_change": last_change(repo.last_db_change), + "last_changeset": last_rev(repo.repo_name, cs_cache), + "raw_tip": cs_cache.get('revision'), + "desc": desc(repo.description), + "owner": h.person(repo.user.username), + "rss": rss_lnk(repo.repo_name), + "atom": atom_lnk(repo.repo_name), + + } + if admin: + row.update({ + "action": repo_actions(repo.repo_name), + "owner": owner_actions(repo.user.user_id, + h.person(repo.user.username)) + }) + repos_data.append(row) + + return { + "totalRecords": len(repos_list), + "startIndex": 0, + "sort": "name", + "dir": "asc", + "records": repos_data + } + def _get_defaults(self, repo_name): """ Get's information about repository, and returns a dict for @@ -339,9 +445,9 @@ class RepoModel(BaseModel): copy_fork_permissions = form_data.get('copy_permissions') fork_of = form_data.get('fork_parent_id') - ##defaults + ## repo creation defaults, private and repo_type are filled in form defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True) - enable_statistics = defs.get('repo_enable_statistic') + enable_statistics = defs.get('repo_enable_statistics') enable_locking = defs.get('repo_enable_locking') enable_downloads = defs.get('repo_enable_downloads') diff --git a/rhodecode/model/repos_group.py b/rhodecode/model/repos_group.py --- a/rhodecode/model/repos_group.py +++ b/rhodecode/model/repos_group.py @@ -273,7 +273,7 @@ class ReposGroupModel(BaseModel): self.sa.delete(repos_group) self.__delete_group(repos_group, force_delete) except: - log.exception('Error removing repos_group %s' % repos_group) + log.error('Error removing repos_group %s' % repos_group) raise def delete_permission(self, repos_group, obj, obj_type, recursive): diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -230,7 +230,7 @@ class ScmModel(BaseModel): # name need to be decomposed and put back together using the / # since this is internal storage separator for rhodecode - name = Repository.url_sep().join(name.split(os.sep)) + name = Repository.normalize_repo_name(name) try: if name in repos: @@ -292,6 +292,9 @@ class ScmModel(BaseModel): :param repo_name: this repo that should invalidation take place """ CacheInvalidation.set_invalidate(repo_name=repo_name) + repo = Repository.get_by_repo_name(repo_name) + if repo: + repo.update_changeset_cache() def toggle_following_repo(self, follow_repo_id, user_id): diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -27,7 +27,6 @@ import logging import traceback import itertools import collections -import functools from pylons import url from pylons.i18n.translation import _ @@ -40,7 +39,7 @@ from rhodecode.model import BaseModel from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \ UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \ Notification, RepoGroup, UserRepoGroupToPerm, UsersGroupRepoGroupToPerm, \ - UserEmailMap + UserEmailMap, UserIpMap from rhodecode.lib.exceptions import DefaultUserException, \ UserOwnsReposException @@ -294,30 +293,6 @@ class UserModel(BaseModel): log.error(traceback.format_exc()) raise - def update_my_account(self, user_id, form_data): - from rhodecode.lib.auth import get_crypt_password - try: - user = self.get(user_id, cache=False) - if user.username == 'default': - raise DefaultUserException( - _("You can't Edit this user since it's" - " crucial for entire application") - ) - for k, v in form_data.items(): - if k == 'new_password' and v: - user.password = get_crypt_password(v) - user.api_key = generate_api_key(user.username) - else: - if k == 'firstname': - k = 'name' - if k not in ['admin', 'active']: - setattr(user, k, v) - - self.sa.add(user) - except: - log.error(traceback.format_exc()) - raise - def delete(self, user): user = self._get_user(user) @@ -705,3 +680,33 @@ class UserModel(BaseModel): obj = UserEmailMap.query().get(email_id) if obj: self.sa.delete(obj) + + def add_extra_ip(self, user, ip): + """ + Adds ip address to UserIpMap + + :param user: + :param ip: + """ + from rhodecode.model import forms + form = forms.UserExtraIpForm()() + data = form.to_python(dict(ip=ip)) + user = self._get_user(user) + + obj = UserIpMap() + obj.user = user + obj.ip_addr = data['ip'] + self.sa.add(obj) + return obj + + def delete_extra_ip(self, user, ip_id): + """ + Removes ip address from UserIpMap + + :param user: + :param ip_id: + """ + user = self._get_user(user) + obj = UserIpMap.query().get(ip_id) + if obj: + self.sa.delete(obj) diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py --- a/rhodecode/model/validators.py +++ b/rhodecode/model/validators.py @@ -11,7 +11,7 @@ from webhelpers.pylonslib.secure_form im from formencode.validators import ( UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, - NotEmpty + NotEmpty, IPAddress, CIDR ) from rhodecode.lib.compat import OrderedSet from rhodecode.lib.utils import repo_name_slug @@ -23,7 +23,7 @@ from rhodecode.lib.auth import HasReposG # silence warnings and pylint UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \ - NotEmpty + NotEmpty, IPAddress, CIDR log = logging.getLogger(__name__) @@ -566,7 +566,7 @@ def ValidPerms(type_='repo'): def ValidSettings(): class _validator(formencode.validators.FancyValidator): def _to_python(self, value, state): - # settings form for users that are not admin + # settings form for users that are not admin # can't edit certain parameters, it's extra backup if they mangle # with forms @@ -706,3 +706,40 @@ def NotReviewedRevisions(repo_id): ) return _validator + + +def ValidIp(): + class _validator(CIDR): + messages = dict( + badFormat=_('Please enter a valid IP address (a.b.c.d)'), + illegalOctets=_('The octets must be within the range of 0-255' + ' (not %(octet)r)'), + illegalBits=_('The network size (bits) must be within the range' + ' of 0-32 (not %(bits)r)')) + + def validate_python(self, value, state): + try: + # Split into octets and bits + if '/' in value: # a.b.c.d/e + addr, bits = value.split('/') + else: # a.b.c.d + addr, bits = value, 32 + # Use IPAddress validator to validate the IP part + IPAddress.validate_python(self, addr, state) + # Bits (netmask) correct? + if not 0 <= int(bits) <= 32: + raise formencode.Invalid( + self.message('illegalBits', state, bits=bits), + value, state) + # Splitting faild: wrong syntax + except ValueError: + raise formencode.Invalid(self.message('badFormat', state), + value, state) + + def to_python(self, value, state): + v = super(_validator, self).to_python(value, state) + #if IP doesn't end with a mask, add /32 + if '/' not in value: + v += '/32' + return v + return _validator diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -2002,7 +2002,6 @@ a.metatag[tag="license"]:hover { } #login div.title { - width: 420px; clear: both; overflow: hidden; position: relative; @@ -2021,7 +2020,6 @@ a.metatag[tag="license"]:hover { } #login div.inner { - width: 380px; background: #FFF url("../images/login.png") no-repeat top left; border-top: none; border-bottom: none; @@ -2038,7 +2036,6 @@ a.metatag[tag="license"]:hover { } #login div.form div.fields div.field div.input input { - width: 176px; background: #FFF; border-top: 1px solid #b3b3b3; border-left: 1px solid #b3b3b3; @@ -2781,7 +2778,9 @@ h3.files_location { margin: 0px 2px; } -.right .logtags .branchtag,.logtags .branchtag { +.right .logtags .branchtag, +.logtags .branchtag, +.spantag { padding: 1px 3px 1px 3px; background-color: #bfbfbf; font-size: 10px; @@ -3238,7 +3237,7 @@ table.code-browser .submodule-dir { } .edit_icon { - background: url("../images/icons/folder_edit.png") no-repeat scroll 3px; + background: url("../images/icons/application_form_edit.png") no-repeat scroll 3px; padding-left: 20px; padding-top: 0px; text-align: left; @@ -4040,6 +4039,22 @@ div#legend_container table td,div#legend float: left } +.ips_wrap{ + padding: 0px 20px; +} + +.ips_wrap .ip_entry{ + height: 30px; + padding:0px 0px 0px 10px; +} +.ips_wrap .ip_entry .ip{ + float: left +} +.ips_wrap .ip_entry .ip_action{ + float: left +} + + /*README STYLE*/ div.readme { diff --git a/rhodecode/public/js/rhodecode.js b/rhodecode/public/js/rhodecode.js --- a/rhodecode/public/js/rhodecode.js +++ b/rhodecode/public/js/rhodecode.js @@ -334,7 +334,7 @@ var show_changeset_tooltip = function(){ YUD.setAttribute(target, 'title',_TM['loading...']); YAHOO.yuitip.main.set_listeners(target); YAHOO.yuitip.main.show_yuitip(e, target); - ajaxGET('/changeset_info/{0}/{1}'.format(repo_name,rid), success) + ajaxGET(LAZY_CS_URL.replace('__NAME__',repo_name).replace('__REV__', rid), success) } }); }; @@ -416,7 +416,6 @@ YAHOO.yuitip.main = { }, init: function(){ - yt._tooltip = ''; yt.tipBox = yt.$('tip-box'); if(!yt.tipBox){ yt.tipBox = document.createElement('div'); @@ -457,7 +456,7 @@ YAHOO.yuitip.main = { if(yt.tipText !== ''){ // save org title - yt._tooltip = yt.tipText; + YUD.setAttribute(el, 'tt_title', yt.tipText); // reset title to not show org tooltips YUD.setAttribute(el, 'title', ''); @@ -495,7 +494,7 @@ YAHOO.yuitip.main = { } else { YUD.setStyle(yt.tipBox, 'display', 'none'); } - YUD.setAttribute(el,'title', yt._tooltip); + YUD.setAttribute(el,'title', YUD.getAttribute(el, 'tt_title')); } } diff --git a/rhodecode/templates/admin/admin.html b/rhodecode/templates/admin/admin.html --- a/rhodecode/templates/admin/admin.html +++ b/rhodecode/templates/admin/admin.html @@ -53,4 +53,3 @@ YUE.on('filter_form','submit',function(e fix_j_filter_width(YUD.get('j_filter').value.length); - diff --git a/rhodecode/templates/admin/admin_log.html b/rhodecode/templates/admin/admin_log.html --- a/rhodecode/templates/admin/admin_log.html +++ b/rhodecode/templates/admin/admin_log.html @@ -16,7 +16,7 @@ ${h.link_to(l.user.username,h.url('edit_user', id=l.user.user_id))} %else: ${l.username} - %endif + %endif ${h.action_parser(l)[0]()}
diff --git a/rhodecode/templates/admin/permissions/permissions.html b/rhodecode/templates/admin/permissions/permissions.html --- a/rhodecode/templates/admin/permissions/permissions.html +++ b/rhodecode/templates/admin/permissions/permissions.html @@ -16,7 +16,7 @@ <%def name="main()"> -
+
${self.breadcrumbs()} @@ -89,10 +89,127 @@
- ${h.submit('set',_('set'),class_="ui-btn large")} + ${h.submit('save',_('Save'),class_="ui-btn large")} + ${h.reset('reset',_('Reset'),class_="ui-btn large")}
${h.end_form()} + +
+ +
+
${_('Default User Permissions')}
+
+ + ## permissions overview +
+ %for section in sorted(c.perm_user.permissions.keys()): +
${section.replace("_"," ").capitalize()}
+ %if not c.perm_user.permissions[section]: + ${_('Nothing here yet')} + %else: +
+ + + + + + + + + %for k in c.perm_user.permissions[section]: + <% + if section != 'global': + section_perm = c.perm_user.permissions[section].get(k) + _perm = section_perm.split('.')[-1] + else: + _perm = section_perm = None + %> + + + + + + %endfor + +
${_('Name')}${_('Permission')}${_('Edit Permission')}
+ %if section == 'repositories': + ${k} + %elif section == 'repositories_groups': + ${k} + %else: + ${h.get_permission_name(k)} + %endif + + %if section == 'global': + ${h.bool2icon(k.split('.')[-1] != 'none')} + %else: + ${section_perm} + %endif + + %if section == 'repositories': + ${_('edit')} + %elif section == 'repositories_groups': + ${_('edit')} + %else: + -- + %endif +
+
+ %endif + %endfor +
+
+
+ +
+
${_('Allowed IP addresses')}
+
+ +
+ + %if c.user_ip_map: + %for ip in c.user_ip_map: + + + + + + %endfor + %else: + + %endif +
${ip.ip_addr}
${h.ip_range(ip.ip_addr)}
+ ${h.form(url('user_ips_delete', id=c.user.user_id),method='delete')} + ${h.hidden('del_ip',ip.ip_id)} + ${h.hidden('default_user', 'True')} + ${h.submit('remove_',_('delete'),id="remove_ip_%s" % ip.ip_id, + class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this ip: %s') % ip.ip_addr+"');")} + ${h.end_form()} +
${_('All IP addresses are allowed')}
+
+ + ${h.form(url('user_ips', id=c.user.user_id),method='put')} +
+ +
+
+
+ +
+
+ ${h.hidden('default_user', 'True')} + ${h.text('new_ip', class_='medium')} +
+
+
+ ${h.submit('save',_('Add'),class_="ui-btn large")} + ${h.reset('reset',_('Reset'),class_="ui-btn large")} +
+
+
+ ${h.end_form()} +
diff --git a/rhodecode/templates/admin/repos/repos.html b/rhodecode/templates/admin/repos/repos.html --- a/rhodecode/templates/admin/repos/repos.html +++ b/rhodecode/templates/admin/repos/repos.html @@ -40,6 +40,7 @@ {key:"raw_name"}, {key:"name"}, {key:"desc"}, + {key:"last_changeset"}, {key:"owner"}, {key:"action"}, ] @@ -70,6 +71,8 @@ {key:"name",label:"${_('Name')}",sortable:true, sortOptions: { sortFunction: nameSort }}, {key:"desc",label:"${_('Description')}",sortable:true}, + {key:"last_changeset",label:"${_('Tip')}",sortable:true, + sortOptions: { sortFunction: revisionSort }}, {key:"owner",label:"${_('Owner')}",sortable:true}, {key:"action",label:"${_('Action')}",sortable:false}, ]; @@ -77,7 +80,7 @@ var myDataTable = new YAHOO.widget.DataTable("repos_list_wrap", myColumnDefs, myDataSource,{ sortedBy:{key:"name",dir:"asc"}, paginator: new YAHOO.widget.Paginator({ - rowsPerPage: 15, + rowsPerPage: 25, alwaysVisible: false, template : "{PreviousPageLink} {FirstPageLink} {PageLinks} {LastPageLink} {NextPageLink}", pageLinks: 5, @@ -111,7 +114,7 @@ // Reset sort var state = myDataTable.getState(); - state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC}; + state.sortedBy = {key:'name', dir:YAHOO.widget.DataTable.CLASS_ASC}; // Get filtered data myDataSource.sendRequest(YUD.get('q_filter').value,{ @@ -123,7 +126,11 @@ }; YUE.on('q_filter','click',function(){ - YUD.get('q_filter').value = ''; + if(!YUD.hasClass('q_filter', 'loaded')){ + YUD.get('q_filter').value = ''; + //TODO: load here full list later to do search within groups + YUD.addClass('q_filter', 'loaded'); + } }); YUE.on('q_filter','keyup',function (e) { diff --git a/rhodecode/templates/admin/users/user_edit.html b/rhodecode/templates/admin/users/user_edit.html --- a/rhodecode/templates/admin/users/user_edit.html +++ b/rhodecode/templates/admin/users/user_edit.html @@ -43,7 +43,11 @@ ${c.user.api_key} - +
+
+ ${c.perm_user.ip_addr or "?"} +
+
@@ -271,7 +275,7 @@
- +
${h.text('new_email', class_='medium')} @@ -285,4 +289,52 @@
${h.end_form()}
+
+ +
+
${_('Allowed IP addresses')}
+
+ +
+ + %if c.user_ip_map: + %for ip in c.user_ip_map: + + + + + + %endfor + %else: + + %endif +
${ip.ip_addr}
${h.ip_range(ip.ip_addr)}
+ ${h.form(url('user_ips_delete', id=c.user.user_id),method='delete')} + ${h.hidden('del_ip',ip.ip_id)} + ${h.submit('remove_',_('delete'),id="remove_ip_%s" % ip.ip_id, + class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this ip: %s') % ip.ip_addr+"');")} + ${h.end_form()} +
${_('All IP addresses are allowed')}
+
+ + ${h.form(url('user_ips', id=c.user.user_id),method='put')} +
+ +
+
+
+ +
+
+ ${h.text('new_ip', class_='medium')} +
+
+
+ ${h.submit('save',_('Add'),class_="ui-btn large")} + ${h.reset('reset',_('Reset'),class_="ui-btn large")} +
+
+
+ ${h.end_form()} +
diff --git a/rhodecode/templates/admin/users/user_edit_my_account.html b/rhodecode/templates/admin/users/user_edit_my_account.html --- a/rhodecode/templates/admin/users/user_edit_my_account.html +++ b/rhodecode/templates/admin/users/user_edit_my_account.html @@ -48,7 +48,7 @@
-
+
%for section in sorted(c.rhodecode_user.permissions.keys()):
${section.replace("_"," ").capitalize()}
@@ -94,30 +94,26 @@
%endfor
- - - diff --git a/rhodecode/templates/admin/users/user_edit_my_account_form.html b/rhodecode/templates/admin/users/user_edit_my_account_form.html --- a/rhodecode/templates/admin/users/user_edit_my_account_form.html +++ b/rhodecode/templates/admin/users/user_edit_my_account_form.html @@ -26,7 +26,11 @@
- ${h.text('username',class_="medium")} + %if c.ldap_dn: + ${h.text('username',class_='medium disabled', readonly="readonly")} + %else: + ${h.text('username',class_='medium')} + %endif:
diff --git a/rhodecode/templates/admin/users/user_edit_my_account_repos.html b/rhodecode/templates/admin/users/user_edit_my_account_repos.html --- a/rhodecode/templates/admin/users/user_edit_my_account_repos.html +++ b/rhodecode/templates/admin/users/user_edit_my_account_repos.html @@ -1,46 +0,0 @@ -
- - - - - - - - - - - <%namespace name="dt" file="/data_table/_dt_elements.html"/> - %if c.user_repos: - %for repo in c.user_repos: - - ##QUICK MENU - - ##REPO NAME AND ICONS - - ##LAST REVISION - - - - - %endfor - %else: -
- ${_('No repositories yet')} - %if h.HasPermissionAny('hg.admin','hg.create.repository')(): - ${h.link_to(_('create one now'),h.url('admin_settings_create_repository'),class_="ui-btn")} - %endif -
- %endif - -
${_('Name')}${_('Revision')}${_('Action')}${_('Action')}
- ${dt.quick_menu(repo['name'])} - - ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],h.AttributeDict(repo['dbrepo_fork']))} - - ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])} - ${_('private')} - ${h.form(url('repo_settings_delete', repo_name=repo['name']),method='delete')} - ${h.submit('remove_%s' % repo['name'],'',class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo['name']+"');")} - ${h.end_form()} -
-
diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -54,6 +54,7 @@ }; var _TM = TRANSLATION_MAP; var TOGGLE_FOLLOW_URL = "${h.url('toggle_following')}"; + var LAZY_CS_URL = "${h.url('changeset_info', repo_name='__NAME__', revision='__REV__')}" +
- - ${_('Watched')} / ${_('My repos')} + +
- %if h.HasPermissionAny('hg.admin','hg.create.repository')(): -
- - -
- %if c.following: - - - - - - - %for entry in c.following: - - - - %endfor - -
${_('Name')}
- %if entry.follows_user_id: - ${_('user')} - ${entry.follows_user.full_contact} - %endif - - %if entry.follows_repo_id: -
- - -
+ + - %if h.is_hg(entry.follows_repository): - ${_('Mercurial repository')} - %elif h.is_git(entry.follows_repository): - ${_('Git repository')} - %endif - - %if entry.follows_repository.private and c.visual.show_private_icon: - ${_('private repository')} - %elif not entry.follows_repository.private and c.visual.show_public_icon: - ${_('public repository')} - %endif - - ${h.link_to(entry.follows_repository.repo_name,h.url('summary_home',repo_name=entry.follows_repository.repo_name))} - - %endif -
- %else: -
- ${_('You are not following any users or repositories')} -
- %endif +
+
+
diff --git a/rhodecode/templates/journal/journal_page_repos.html b/rhodecode/templates/journal/journal_page_repos.html deleted file mode 100644 --- a/rhodecode/templates/journal/journal_page_repos.html +++ /dev/null @@ -1,47 +0,0 @@ -%if c.user_repos: -
- - - - - - - - - - - <%namespace name="dt" file="/data_table/_dt_elements.html"/> - %for repo in c.user_repos: - - ##QUICK MENU - - ##REPO NAME AND ICONS - - ##LAST REVISION - - ## - - - - %endfor - -
${_('Name')}${_('Revision')}${_('Action')}${_('Action')}
- ${dt.quick_menu(repo['name'])} - - ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],h.AttributeDict(repo['dbrepo_fork']))} - - ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])} - ${_('private')} - ${h.form(url('repo_settings_delete', repo_name=repo['name']),method='delete')} - ${h.submit('remove_%s' % repo['name'],'',class_="delete_icon action_button",onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo['name']+"');")} - ${h.end_form()} -
-
- %else: -
- ${_('No repositories yet')} - %if h.HasPermissionAny('hg.admin','hg.create.repository')(): - ${h.link_to(_('create one now'),h.url('admin_settings_create_repository'),class_="ui-btn")} - %endif -
- %endif diff --git a/rhodecode/templates/pullrequests/pullrequest_show.html b/rhodecode/templates/pullrequests/pullrequest_show.html --- a/rhodecode/templates/pullrequests/pullrequest_show.html +++ b/rhodecode/templates/pullrequests/pullrequest_show.html @@ -51,6 +51,24 @@ %endif
+
+
+ +
+
+
+ ##%if h.is_hg(c.pull_request.org_repo): + ## ${_('Mercurial repository')} + ##%elif h.is_git(c.pull_request.org_repo): + ## ${_('Git repository')} + ##%endif + ${c.pull_request.org_ref_parts[0]} + : + ${c.pull_request.org_ref_parts[1]} + ${c.pull_request.org_repo.clone_url()} +
+
+
${h.literal(c.pull_request.description)}
@@ -198,7 +216,7 @@ // inject comments into they proper positions var file_comments = YUQ('.inline-comment-placeholder'); renderInlineComments(file_comments); - + YUE.on(YUD.get('update_pull_request'),'click',function(e){ updateReviewers(); }) diff --git a/rhodecode/templates/settings/repo_settings.html b/rhodecode/templates/settings/repo_settings.html --- a/rhodecode/templates/settings/repo_settings.html +++ b/rhodecode/templates/settings/repo_settings.html @@ -94,7 +94,7 @@ ${h.submit('save',_('Save'),class_="ui-btn large")} ${h.reset('reset',_('Reset'),class_="ui-btn large")} - + ${h.end_form()} diff --git a/rhodecode/tests/api/api_base.py b/rhodecode/tests/api/api_base.py --- a/rhodecode/tests/api/api_base.py +++ b/rhodecode/tests/api/api_base.py @@ -59,13 +59,13 @@ def destroy_users_group(name=TEST_USERS_ Session().commit() -def create_repo(repo_name, repo_type): +def create_repo(repo_name, repo_type, owner=None): # create new repo form_data = _get_repo_create_params( repo_name_full=repo_name, repo_description='description %s' % repo_name, ) - cur_user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) + cur_user = UserModel().get_by_username(owner or TEST_USER_ADMIN_LOGIN) r = RepoModel().create(form_data, cur_user) Session().commit() return r @@ -93,7 +93,7 @@ class BaseTestApi(object): def setUpClass(self): self.usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) self.apikey = self.usr.api_key - self.TEST_USER = UserModel().create_or_update( + self.test_user = UserModel().create_or_update( username='test-api', password='test', email='test@api.rhodecode.org', @@ -101,7 +101,8 @@ class BaseTestApi(object): lastname='last' ) Session().commit() - self.TEST_USER_LOGIN = self.TEST_USER.username + self.TEST_USER_LOGIN = self.test_user.username + self.apikey_regular = self.test_user.api_key @classmethod def teardownClass(self): @@ -148,12 +149,40 @@ class BaseTestApi(object): self._compare_error(id_, expected, given=response.body) def test_api_missing_non_optional_param(self): - id_, params = _build_data(self.apikey, 'get_user') + id_, params = _build_data(self.apikey, 'get_repo') + response = api_call(self, params) + + expected = 'Missing non optional `repoid` arg in JSON DATA' + self._compare_error(id_, expected, given=response.body) + + def test_api_missing_non_optional_param_args_null(self): + id_, params = _build_data(self.apikey, 'get_repo') + params = params.replace('"args": {}', '"args": null') response = api_call(self, params) - expected = 'Missing non optional `userid` arg in JSON DATA' + expected = 'Missing non optional `repoid` arg in JSON DATA' + self._compare_error(id_, expected, given=response.body) + + def test_api_missing_non_optional_param_args_bad(self): + id_, params = _build_data(self.apikey, 'get_repo') + params = params.replace('"args": {}', '"args": 1') + response = api_call(self, params) + + expected = 'Missing non optional `repoid` arg in JSON DATA' self._compare_error(id_, expected, given=response.body) + def test_api_args_is_null(self): + id_, params = _build_data(self.apikey, 'get_users',) + params = params.replace('"args": {}', '"args": null') + response = api_call(self, params) + self.assertEqual(response.status, '200 OK') + + def test_api_args_is_bad(self): + id_, params = _build_data(self.apikey, 'get_users',) + params = params.replace('"args": {}', '"args": 1') + response = api_call(self, params) + self.assertEqual(response.status, '200 OK') + def test_api_get_users(self): id_, params = _build_data(self.apikey, 'get_users',) response = api_call(self, params) @@ -184,6 +213,36 @@ class BaseTestApi(object): expected = "user `%s` does not exist" % 'trololo' self._compare_error(id_, expected, given=response.body) + def test_api_get_user_without_giving_userid(self): + id_, params = _build_data(self.apikey, 'get_user') + response = api_call(self, params) + + usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN) + ret = usr.get_api_data() + ret['permissions'] = AuthUser(usr.user_id).permissions + + expected = ret + self._compare_ok(id_, expected, given=response.body) + + def test_api_get_user_without_giving_userid_non_admin(self): + id_, params = _build_data(self.apikey_regular, 'get_user') + response = api_call(self, params) + + usr = UserModel().get_by_username(self.TEST_USER_LOGIN) + ret = usr.get_api_data() + ret['permissions'] = AuthUser(usr.user_id).permissions + + expected = ret + self._compare_ok(id_, expected, given=response.body) + + def test_api_get_user_with_giving_userid_non_admin(self): + id_, params = _build_data(self.apikey_regular, 'get_user', + userid=self.TEST_USER_LOGIN) + response = api_call(self, params) + + expected = 'userid is not the same as your user' + self._compare_error(id_, expected, given=response.body) + def test_api_pull(self): #TODO: issues with rhodecode_extras here.. not sure why ! pass @@ -237,6 +296,42 @@ class BaseTestApi(object): % (TEST_USER_ADMIN_LOGIN, self.REPO, True)) self._compare_ok(id_, expected, given=response.body) + def test_api_lock_repo_lock_aquire_by_non_admin(self): + repo_name = 'api_delete_me' + create_repo(repo_name, self.REPO_TYPE, owner=self.TEST_USER_LOGIN) + try: + id_, params = _build_data(self.apikey_regular, 'lock', + repoid=repo_name, + locked=True) + response = api_call(self, params) + expected = ('User `%s` set lock state for repo `%s` to `%s`' + % (self.TEST_USER_LOGIN, repo_name, True)) + self._compare_ok(id_, expected, given=response.body) + finally: + destroy_repo(repo_name) + + def test_api_lock_repo_lock_aquire_non_admin_with_userid(self): + repo_name = 'api_delete_me' + create_repo(repo_name, self.REPO_TYPE, owner=self.TEST_USER_LOGIN) + try: + id_, params = _build_data(self.apikey_regular, 'lock', + userid=TEST_USER_ADMIN_LOGIN, + repoid=repo_name, + locked=True) + response = api_call(self, params) + expected = 'userid is not the same as your user' + self._compare_error(id_, expected, given=response.body) + finally: + destroy_repo(repo_name) + + def test_api_lock_repo_lock_aquire_non_admin_not_his_repo(self): + id_, params = _build_data(self.apikey_regular, 'lock', + repoid=self.REPO, + locked=True) + response = api_call(self, params) + expected = 'repository `%s` does not exist' % (self.REPO) + self._compare_error(id_, expected, given=response.body) + def test_api_lock_repo_lock_release(self): id_, params = _build_data(self.apikey, 'lock', userid=TEST_USER_ADMIN_LOGIN, @@ -247,6 +342,15 @@ class BaseTestApi(object): % (TEST_USER_ADMIN_LOGIN, self.REPO, False)) self._compare_ok(id_, expected, given=response.body) + def test_api_lock_repo_lock_aquire_optional_userid(self): + id_, params = _build_data(self.apikey, 'lock', + repoid=self.REPO, + locked=True) + response = api_call(self, params) + expected = ('User `%s` set lock state for repo `%s` to `%s`' + % (TEST_USER_ADMIN_LOGIN, self.REPO, True)) + self._compare_ok(id_, expected, given=response.body) + @mock.patch.object(Repository, 'lock', crash) def test_api_lock_error(self): id_, params = _build_data(self.apikey, 'lock', @@ -457,6 +561,48 @@ class BaseTestApi(object): self._compare_ok(id_, expected, given=response.body) destroy_users_group(new_group) + def test_api_get_repo_by_non_admin(self): + id_, params = _build_data(self.apikey, 'get_repo', + repoid=self.REPO) + response = api_call(self, params) + + repo = RepoModel().get_by_repo_name(self.REPO) + ret = repo.get_api_data() + + members = [] + for user in repo.repo_to_perm: + perm = user.permission.permission_name + user = user.user + user_data = user.get_api_data() + user_data['type'] = "user" + user_data['permission'] = perm + members.append(user_data) + + for users_group in repo.users_group_to_perm: + perm = users_group.permission.permission_name + users_group = users_group.users_group + users_group_data = users_group.get_api_data() + users_group_data['type'] = "users_group" + users_group_data['permission'] = perm + members.append(users_group_data) + + ret['members'] = members + + expected = ret + self._compare_ok(id_, expected, given=response.body) + + def test_api_get_repo_by_non_admin_no_permission_to_repo(self): + RepoModel().grant_user_permission(repo=self.REPO, + user=self.TEST_USER_LOGIN, + perm='repository.none') + + id_, params = _build_data(self.apikey_regular, 'get_repo', + repoid=self.REPO) + response = api_call(self, params) + + expected = 'repository `%s` does not exist' % (self.REPO) + self._compare_error(id_, expected, given=response.body) + def test_api_get_repo_that_doesn_not_exist(self): id_, params = _build_data(self.apikey, 'get_repo', repoid='no-such-repo') @@ -478,6 +624,18 @@ class BaseTestApi(object): expected = ret self._compare_ok(id_, expected, given=response.body) + def test_api_get_repos_non_admin(self): + id_, params = _build_data(self.apikey_regular, 'get_repos') + response = api_call(self, params) + + result = [] + for repo in RepoModel().get_all_user_repos(self.TEST_USER_LOGIN): + result.append(repo.get_api_data()) + ret = jsonify(result) + + expected = ret + self._compare_ok(id_, expected, given=response.body) + @parameterized.expand([('all', 'all'), ('dirs', 'dirs'), ('files', 'files'), ]) @@ -560,6 +718,56 @@ class BaseTestApi(object): expected = 'user `%s` does not exist' % owner self._compare_error(id_, expected, given=response.body) + def test_api_create_repo_dont_specify_owner(self): + repo_name = 'api-repo' + owner = 'i-dont-exist' + id_, params = _build_data(self.apikey, 'create_repo', + repo_name=repo_name, + repo_type='hg', + ) + response = api_call(self, params) + + repo = RepoModel().get_by_repo_name(repo_name) + ret = { + 'msg': 'Created new repository `%s`' % repo_name, + 'repo': jsonify(repo.get_api_data()) + } + expected = ret + self._compare_ok(id_, expected, given=response.body) + destroy_repo(repo_name) + + def test_api_create_repo_by_non_admin(self): + repo_name = 'api-repo' + owner = 'i-dont-exist' + id_, params = _build_data(self.apikey_regular, 'create_repo', + repo_name=repo_name, + repo_type='hg', + ) + response = api_call(self, params) + + repo = RepoModel().get_by_repo_name(repo_name) + ret = { + 'msg': 'Created new repository `%s`' % repo_name, + 'repo': jsonify(repo.get_api_data()) + } + expected = ret + self._compare_ok(id_, expected, given=response.body) + destroy_repo(repo_name) + + def test_api_create_repo_by_non_admin_specify_owner(self): + repo_name = 'api-repo' + owner = 'i-dont-exist' + id_, params = _build_data(self.apikey_regular, 'create_repo', + repo_name=repo_name, + repo_type='hg', + owner=owner + ) + response = api_call(self, params) + + expected = 'Only RhodeCode admin can specify `owner` param' + self._compare_error(id_, expected, given=response.body) + destroy_repo(repo_name) + def test_api_create_repo_exists(self): repo_name = self.REPO id_, params = _build_data(self.apikey, 'create_repo', @@ -598,6 +806,35 @@ class BaseTestApi(object): expected = ret self._compare_ok(id_, expected, given=response.body) + def test_api_delete_repo_by_non_admin(self): + repo_name = 'api_delete_me' + create_repo(repo_name, self.REPO_TYPE, owner=self.TEST_USER_LOGIN) + try: + id_, params = _build_data(self.apikey_regular, 'delete_repo', + repoid=repo_name,) + response = api_call(self, params) + + ret = { + 'msg': 'Deleted repository `%s`' % repo_name, + 'success': True + } + expected = ret + self._compare_ok(id_, expected, given=response.body) + finally: + destroy_repo(repo_name) + + def test_api_delete_repo_by_non_admin_no_permission(self): + repo_name = 'api_delete_me' + create_repo(repo_name, self.REPO_TYPE) + try: + id_, params = _build_data(self.apikey_regular, 'delete_repo', + repoid=repo_name,) + response = api_call(self, params) + expected = 'repository `%s` does not exist' % (repo_name) + self._compare_error(id_, expected, given=response.body) + finally: + destroy_repo(repo_name) + def test_api_delete_repo_exception_occurred(self): repo_name = 'api_delete_me' create_repo(repo_name, self.REPO_TYPE) @@ -630,6 +867,49 @@ class BaseTestApi(object): self._compare_ok(id_, expected, given=response.body) destroy_repo(fork_name) + def test_api_fork_repo_non_admin(self): + fork_name = 'api-repo-fork' + id_, params = _build_data(self.apikey_regular, 'fork_repo', + repoid=self.REPO, + fork_name=fork_name, + ) + response = api_call(self, params) + + ret = { + 'msg': 'Created fork of `%s` as `%s`' % (self.REPO, + fork_name), + 'success': True + } + expected = ret + self._compare_ok(id_, expected, given=response.body) + destroy_repo(fork_name) + + def test_api_fork_repo_non_admin_specify_owner(self): + fork_name = 'api-repo-fork' + id_, params = _build_data(self.apikey_regular, 'fork_repo', + repoid=self.REPO, + fork_name=fork_name, + owner=TEST_USER_ADMIN_LOGIN, + ) + response = api_call(self, params) + expected = 'Only RhodeCode admin can specify `owner` param' + self._compare_error(id_, expected, given=response.body) + destroy_repo(fork_name) + + def test_api_fork_repo_non_admin_no_permission_to_fork(self): + RepoModel().grant_user_permission(repo=self.REPO, + user=self.TEST_USER_LOGIN, + perm='repository.none') + fork_name = 'api-repo-fork' + id_, params = _build_data(self.apikey_regular, 'fork_repo', + repoid=self.REPO, + fork_name=fork_name, + ) + response = api_call(self, params) + expected = 'repository `%s` does not exist' % (self.REPO) + self._compare_error(id_, expected, given=response.body) + destroy_repo(fork_name) + def test_api_fork_repo_unknown_owner(self): fork_name = 'api-repo-fork' owner = 'i-dont-exist' diff --git a/rhodecode/tests/functional/test_admin_notifications.py b/rhodecode/tests/functional/test_admin_notifications.py --- a/rhodecode/tests/functional/test_admin_notifications.py +++ b/rhodecode/tests/functional/test_admin_notifications.py @@ -82,6 +82,7 @@ class TestNotificationsController(TestCo response = self.app.delete(url('notification', notification_id= notification.notification_id)) + self.assertEqual(response.body, 'ok') cur_user = User.get(cur_usr_id) self.assertEqual(cur_user.notifications, []) diff --git a/rhodecode/tests/functional/test_compare.py b/rhodecode/tests/functional/test_compare.py --- a/rhodecode/tests/functional/test_compare.py +++ b/rhodecode/tests/functional/test_compare.py @@ -98,7 +98,7 @@ class TestCompareController(TestControll )) try: - response.mustcontain('%s@%s -> %s@%s' % (repo2.repo_name, rev1, repo1.repo_name, rev2)) + response.mustcontain('%s@%s -> %s@%s' % (repo2.repo_name, rev1, repo1.repo_name, rev2)) response.mustcontain("""Showing 2 commits""") response.mustcontain("""1 file changed with 2 insertions and 0 deletions""") @@ -156,7 +156,7 @@ class TestCompareController(TestControll )) try: - response.mustcontain('%s@%s -> %s@%s' % (repo2.repo_name, rev1, repo1.repo_name, rev2)) + response.mustcontain('%s@%s -> %s@%s' % (repo2.repo_name, rev1, repo1.repo_name, rev2)) response.mustcontain("""Showing 2 commits""") response.mustcontain("""1 file changed with 2 insertions and 0 deletions""") @@ -191,7 +191,7 @@ class TestCompareController(TestControll # )) # # try: -# response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2)) +# response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2)) # ## outgoing changesets between those revisions # # response.mustcontain("""r4:2dda4e345fac""" % (HG_REPO)) @@ -226,7 +226,7 @@ class TestCompareController(TestControll # )) # # try: -# response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2)) +# response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2)) # ## outgoing changesets between those revisions # # response.mustcontain("""r4:2dda4e345fac""" % (HG_REPO)) @@ -312,7 +312,7 @@ class TestCompareController(TestControll # )) # # try: -# #response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2)) +# #response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2)) # # #add new commit into parent ! # cs0 = ScmModel().create_node( @@ -336,7 +336,7 @@ class TestCompareController(TestControll # bundle=False # )) # -# response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2)) +# response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2)) # response.mustcontain("""file1-line1-from-fork""") # response.mustcontain("""file2-line1-from-fork""") # response.mustcontain("""file3-line1-from-fork""") diff --git a/rhodecode/tests/functional/test_compare_local.py b/rhodecode/tests/functional/test_compare_local.py --- a/rhodecode/tests/functional/test_compare_local.py +++ b/rhodecode/tests/functional/test_compare_local.py @@ -19,7 +19,7 @@ class TestCompareController(TestControll other_ref_type="tag", other_ref=tag2, )) - response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2)) + response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2)) ## outgoing changesets between tags response.mustcontain('''r112:c5ddebc06eaa''' % HG_REPO) response.mustcontain('''r115:70d4cef8a376''' % HG_REPO) @@ -56,7 +56,7 @@ class TestCompareController(TestControll other_ref=tag2, bundle=False )) - response.mustcontain('%s@%s -> %s@%s' % (GIT_REPO, tag1, GIT_REPO, tag2)) + response.mustcontain('%s@%s -> %s@%s' % (GIT_REPO, tag1, GIT_REPO, tag2)) ## outgoing changesets between tags response.mustcontain('''r113:794bbdd31545''' % GIT_REPO) @@ -92,7 +92,7 @@ class TestCompareController(TestControll other_ref='default', )) - response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO)) + response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO)) # branch are equal response.mustcontain('No files') response.mustcontain('No changesets') @@ -107,7 +107,7 @@ class TestCompareController(TestControll other_ref='master', )) - response.mustcontain('%s@master -> %s@master' % (GIT_REPO, GIT_REPO)) + response.mustcontain('%s@master -> %s@master' % (GIT_REPO, GIT_REPO)) # branch are equal response.mustcontain('No files') response.mustcontain('No changesets') @@ -124,7 +124,7 @@ class TestCompareController(TestControll other_ref_type="rev", other_ref=rev2, )) - response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_REPO, rev2)) + response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_REPO, rev2)) ## outgoing changesets between those revisions response.mustcontain("""r1:%s""" % (HG_REPO, rev2)) @@ -144,7 +144,7 @@ class TestCompareController(TestControll other_ref_type="rev", other_ref=rev2, )) - response.mustcontain('%s@%s -> %s@%s' % (GIT_REPO, rev1, GIT_REPO, rev2)) + response.mustcontain('%s@%s -> %s@%s' % (GIT_REPO, rev1, GIT_REPO, rev2)) ## outgoing changesets between those revisions response.mustcontain("""r1:%s""" % (GIT_REPO, rev2[:12])) response.mustcontain('1 file changed with 7 insertions and 0 deletions') diff --git a/rhodecode/tests/functional/test_home.py b/rhodecode/tests/functional/test_home.py --- a/rhodecode/tests/functional/test_home.py +++ b/rhodecode/tests/functional/test_home.py @@ -3,6 +3,9 @@ from rhodecode.tests import * from rhodecode.model.meta import Session from rhodecode.model.db import User, RhodeCodeSetting, Repository from rhodecode.lib.utils import set_rhodecode_config +from rhodecode.tests.models.common import _make_repo, _make_group +from rhodecode.model.repo import RepoModel +from rhodecode.model.repos_group import ReposGroupModel class TestHomeController(TestController): @@ -61,18 +64,45 @@ merge" class="tooltip" href="/vcs_test_h Session().add(anon) Session().commit() + def _set_l_dash(self, set_to): + self.app.post(url('admin_setting', setting_id='visual'), + params=dict(_method='put', + rhodecode_lightweight_dashboard=set_to,)) + def test_index_with_lightweight_dashboard(self): self.log_user() - - def set_l_dash(set_to): - self.app.post(url('admin_setting', setting_id='visual'), - params=dict(_method='put', - rhodecode_lightweight_dashboard=set_to,)) - - set_l_dash(True) + self._set_l_dash(True) try: response = self.app.get(url(controller='home', action='index')) response.mustcontain("""var data = {"totalRecords": %s""" % len(Repository.getAll())) finally: - set_l_dash(False) + self._set_l_dash(False) + + def test_index_page_on_groups(self): + self.log_user() + _make_repo(name='gr1/repo_in_group', repos_group=_make_group('gr1')) + Session().commit() + response = self.app.get(url('repos_group_home', group_name='gr1')) + + try: + response.mustcontain("""gr1/repo_in_group""") + finally: + RepoModel().delete('gr1/repo_in_group') + ReposGroupModel().delete(repos_group='gr1', force_delete=True) + Session().commit() + + def test_index_page_on_groups_with_lightweight_dashboard(self): + self.log_user() + self._set_l_dash(True) + _make_repo(name='gr1/repo_in_group', repos_group=_make_group('gr1')) + Session().commit() + response = self.app.get(url('repos_group_home', group_name='gr1')) + + try: + response.mustcontain("""gr1/repo_in_group""") + finally: + self._set_l_dash(False) + RepoModel().delete('gr1/repo_in_group') + ReposGroupModel().delete(repos_group='gr1', force_delete=True) + Session().commit() diff --git a/rhodecode/tests/functional/test_journal.py b/rhodecode/tests/functional/test_journal.py --- a/rhodecode/tests/functional/test_journal.py +++ b/rhodecode/tests/functional/test_journal.py @@ -10,10 +10,7 @@ class TestJournalController(TestControll self.log_user() response = self.app.get(url(controller='journal', action='index')) - # Test response... - assert """ %s""" % datetime.date.today() in response.body, 'no info about action journal day' + response.mustcontain("""
%s
""" % datetime.date.today()) def test_stop_following_repository(self): session = self.log_user() diff --git a/rhodecode/tests/models/test_user_permissions_on_repos.py b/rhodecode/tests/models/test_user_permissions_on_repos.py --- a/rhodecode/tests/models/test_user_permissions_on_repos.py +++ b/rhodecode/tests/models/test_user_permissions_on_repos.py @@ -1,1 +1,1 @@ -#TODO; write tests when we activate algo for permissions. \ No newline at end of file +#TODO; write tests when we activate algo for permissions. diff --git a/rhodecode/tests/scripts/test_vcs_operations.py b/rhodecode/tests/scripts/test_vcs_operations.py --- a/rhodecode/tests/scripts/test_vcs_operations.py +++ b/rhodecode/tests/scripts/test_vcs_operations.py @@ -39,6 +39,7 @@ from rhodecode.tests import * from rhodecode.model.db import User, Repository, UserLog from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel +from rhodecode.model.user import UserModel DEBUG = True HOST = '127.0.0.1:5000' # test host @@ -420,3 +421,41 @@ class TestVCSOperations(unittest.TestCas # Session.remove() # r = Repository.get_by_repo_name(GIT_REPO) # assert r.locked == [None, None] + + def test_ip_restriction_hg(self): + user_model = UserModel() + new_ip = user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32') + Session().commit() + clone_url = _construct_url(HG_REPO) + stdout, stderr = Command('/tmp').execute('hg clone', clone_url) + assert 'abort: HTTP Error 403: Forbidden' in stderr + + #release IP restrictions + clone_url = _construct_url(HG_REPO) + user_model.delete_extra_ip(TEST_USER_ADMIN_LOGIN, new_ip.ip_id) + Session().commit() + stdout, stderr = Command('/tmp').execute('hg clone', clone_url) + + assert 'requesting all changes' in stdout + assert 'adding changesets' in stdout + assert 'adding manifests' in stdout + assert 'adding file changes' in stdout + + assert stderr == '' + + def test_ip_restriction_git(self): + user_model = UserModel() + new_ip = user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32') + Session().commit() + clone_url = _construct_url(GIT_REPO) + stdout, stderr = Command('/tmp').execute('git clone', clone_url) + assert 'error: The requested URL returned error: 403 Forbidden' in stderr + + #release IP restrictions + clone_url = _construct_url(GIT_REPO) + user_model.delete_extra_ip(TEST_USER_ADMIN_LOGIN, new_ip.ip_id) + Session().commit() + stdout, stderr = Command('/tmp').execute('git clone', clone_url) + + assert 'Cloning into' in stdout + assert stderr == '' diff --git a/rhodecode/tests/test_libs.py b/rhodecode/tests/test_libs.py --- a/rhodecode/tests/test_libs.py +++ b/rhodecode/tests/test_libs.py @@ -123,15 +123,16 @@ class TestLibs(unittest.TestCase): from rhodecode.lib.utils2 import age n = datetime.datetime.now() delt = lambda *args, **kwargs: datetime.timedelta(*args, **kwargs) + prev_month = n.month - 1 if n.month != 1 else n.month - 2 self.assertEqual(age(n), u'just now') self.assertEqual(age(n - delt(seconds=1)), u'1 second ago') self.assertEqual(age(n - delt(seconds=60 * 2)), u'2 minutes ago') self.assertEqual(age(n - delt(hours=1)), u'1 hour ago') self.assertEqual(age(n - delt(hours=24)), u'1 day ago') self.assertEqual(age(n - delt(hours=24 * 5)), u'5 days ago') - self.assertEqual(age(n - delt(hours=24 * (calendar.mdays[n.month - 1]))), + self.assertEqual(age(n - delt(hours=24 * (calendar.mdays[prev_month]))), u'1 month ago') - self.assertEqual(age(n - delt(hours=24 * (calendar.mdays[n.month - 1] + 2))), + self.assertEqual(age(n - delt(hours=24 * (calendar.mdays[prev_month] + 2))), u'1 month and 2 days ago') self.assertEqual(age(n - delt(hours=24 * 400)), u'1 year and 1 month ago') diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -60,10 +60,10 @@ if sys.version_info < (2, 7): requirements.append("unittest2") if is_windows: - requirements.append("mercurial==2.4.1") + requirements.append("mercurial==2.4.2") else: requirements.append("py-bcrypt") - requirements.append("mercurial==2.4.1") + requirements.append("mercurial==2.4.2") dependency_links = [